Initial commit
This commit is contained in:
11
.claude-plugin/plugin.json
Normal file
11
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "py-plugin",
|
||||||
|
"description": "A plugin stores all skills for py projects",
|
||||||
|
"version": "1.0.14",
|
||||||
|
"author": {
|
||||||
|
"name": "dai.pham"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# py-plugin
|
||||||
|
|
||||||
|
A plugin stores all skills for py projects
|
||||||
117
plugin.lock.json
Normal file
117
plugin.lock.json
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:daispacy/py-claude-marketplace:py-plugin",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "b7986a7d679ee30ced2536138ac6e1e849ef644f",
|
||||||
|
"treeHash": "472ad6a3155f907930352d03b17d5d4ad84f722a42302f2609428c375f3b338a",
|
||||||
|
"generatedAt": "2025-11-28T10:16:00.637217Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "py-plugin",
|
||||||
|
"description": "A plugin stores all skills for py projects",
|
||||||
|
"version": "1.0.14"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "0163fee5cabf53d98222f09ef07dd5414019adb7c79b95275f3a5922b99b0111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "1db18d788dca8a6e9e84aa69f895574fd59f478002328290c6394e22a958a04e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/clean-architecture-review/examples.md",
|
||||||
|
"sha256": "fdc1e9674c26bf97deb7fc3de4556c811ac18632f42bf8e5c178749a135114ac"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/clean-architecture-review/SKILL.md",
|
||||||
|
"sha256": "edd7a0d4c7e77ea1233f5344e3a2ab384f00ea2372b2f7c534de601d1adeb89b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/ios-naming-conventions/examples.md",
|
||||||
|
"sha256": "f3d2203266dcc77a6d8f5ffb2ffd3d87de664d755fc815d5d11d423017bb2395"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/ios-naming-conventions/SKILL.md",
|
||||||
|
"sha256": "4a707fe4830fe2d5555055280839aef492036a3fdb9e73bb942f7c1fbe6b2e7f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-builder/SKILL.md",
|
||||||
|
"sha256": "00ec0273c1a3300dfe5a185567927925b3b1d01c257a4a52eebacc43d23b20b1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/copilot-agent-builder/examples.md",
|
||||||
|
"sha256": "b70b2bf1ef1abcb807de19b0a8884aa85ebc79db218af0253bd4364169b6f382"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/copilot-agent-builder/templates.md",
|
||||||
|
"sha256": "38b930e14ee39e0cb32ba8af3856c915985daca7cc440f0ae6a98d0d60af0fb5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/copilot-agent-builder/SKILL.md",
|
||||||
|
"sha256": "710af310a1ec012ba124e223257a8eaa4ec33fc639e172844096f5c1f6997c70"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/instruments/examples.md",
|
||||||
|
"sha256": "6492d3764fafde1d7babf13d73be4757e386cb1b226fb425eb0cff08dc71c7c3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/instruments/SKILL.md",
|
||||||
|
"sha256": "38f22b0a9544c22eb6259a01ce4fc4908949bcd6b5351d19f5b74a9f9736bbcf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/android-code-review/examples.md",
|
||||||
|
"sha256": "a41c1dd2af1a2e19d93c7bffc1dfad2cd21a3c3b3f2097ce93bf0ebc102cfe02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/android-code-review/SKILL.md",
|
||||||
|
"sha256": "f5b52e305445979c9eef35a756fc7c4bc9bad4a43bf082349c40247759fede2f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/android-code-review/standards.md",
|
||||||
|
"sha256": "4cc63abd13cdf67f8d89cfc322990bfaa343a0367e922c9908c7e20bf2adf002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/ios-code-review/examples.md",
|
||||||
|
"sha256": "5fc609265eb9a87ecd37b76eaef5e5b285032992d48e2218db0ab1017f16504a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/ios-code-review/SKILL.md",
|
||||||
|
"sha256": "da4d171fb45a250e7806247c063b3cd337d2ad940260c1e864fdd1db538e4364"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/rxswift-memory-check/examples.md",
|
||||||
|
"sha256": "ff478403c43b2fa1ebc61fc3bdc3a0709498baf1518e27361140b4b4df6b9bd0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/rxswift-memory-check/SKILL.md",
|
||||||
|
"sha256": "d522e2954a2b99f59f9c9846c57f04b7180b8f2f1e97ef0fbb91a9084d9d68f3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/mcp-tool-generator/examples.md",
|
||||||
|
"sha256": "102c2691dd93bbbd923d702f5fe2317a4bd4eb9e1e9b524c8dedae8b470b1a3b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/mcp-tool-generator/SKILL.md",
|
||||||
|
"sha256": "34ae905365a1c23fcc79d50248875efb23de0334be02a263d3c18fe4d5627fc0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "472ad6a3155f907930352d03b17d5d4ad84f722a42302f2609428c375f3b338a"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
180
skills/android-code-review/SKILL.md
Normal file
180
skills/android-code-review/SKILL.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
name: android-code-review
|
||||||
|
description: Comprehensive Android Kotlin code review for Payoo Android app. Checks Kotlin best practices, coroutines, Clean Architecture, MVVM patterns, lifecycle management, dependency injection, memory management, security, and performance. Use when reviewing Kotlin files, pull requests, or ViewModels, Activities, Fragments, UseCases, and Repositories.
|
||||||
|
allowed-tools: Read, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# Android Code Review
|
||||||
|
|
||||||
|
Expert Android code reviewer for Payoo Android application, specializing in Kotlin, Coroutines, Jetpack components, and Clean Architecture patterns.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- "review android code", "check android file", "review android PR"
|
||||||
|
- Mentions Kotlin/Java files: Activity, Fragment, ViewModel, UseCase, Repository
|
||||||
|
- "code quality", "best practices", "check android standards"
|
||||||
|
- Coroutines, Clean Architecture, MVVM patterns, Jetpack components
|
||||||
|
|
||||||
|
## Review Process
|
||||||
|
|
||||||
|
### Step 1: Identify Scope
|
||||||
|
Determine what to review:
|
||||||
|
- Specific files (e.g., "PaymentViewModel.kt")
|
||||||
|
- Directories (e.g., "payment module")
|
||||||
|
- Git changes (recent commits, PR diff)
|
||||||
|
- Entire module or feature
|
||||||
|
|
||||||
|
### Step 2: Read and Analyze
|
||||||
|
Use Read tool to examine files, checking against 7 core categories.
|
||||||
|
|
||||||
|
### Step 3: Apply Standards
|
||||||
|
|
||||||
|
#### 1. Naming Conventions ✅
|
||||||
|
- **Types**: PascalCase, descriptive (e.g., `PaymentViewModel`)
|
||||||
|
- **Variables**: camelCase (e.g., `paymentAmount`, `isLoading`)
|
||||||
|
- **Constants**: UPPER_SNAKE_CASE (e.g., `MAX_RETRY_COUNT`)
|
||||||
|
- **Booleans**: Prefix with `is`, `has`, `should`, `can`
|
||||||
|
- **No abbreviations** except URL, ID, API, HTTP, UI
|
||||||
|
- **Views**: Include type suffix (e.g., `amountEditText`, `submitButton`)
|
||||||
|
|
||||||
|
#### 2. Kotlin Best Practices 🎯
|
||||||
|
- **Null Safety**: Use `?` and `!!` appropriately, prefer safe calls
|
||||||
|
- **Data Classes**: Use for DTOs and models
|
||||||
|
- **Sealed Classes**: For state management and result types
|
||||||
|
- **Extension Functions**: For reusable utilities
|
||||||
|
- **Scope Functions**: Use `let`, `apply`, `run`, `also`, `with` correctly
|
||||||
|
- **Immutability**: Prefer `val` over `var`
|
||||||
|
|
||||||
|
#### 3. Coroutines Patterns 🔄
|
||||||
|
- **Scope**: Use `viewModelScope`, `lifecycleScope` appropriately
|
||||||
|
- **Dispatchers**: `Dispatchers.IO` for network/DB, `Dispatchers.Main` for UI
|
||||||
|
- **Cancellation**: All coroutines properly cancelled
|
||||||
|
- **Error Handling**: Use `try-catch` or `runCatching` in coroutines
|
||||||
|
- **Flows**: Use `StateFlow`, `SharedFlow` for reactive data
|
||||||
|
- **No blocking**: Never use `runBlocking` in production code
|
||||||
|
|
||||||
|
#### 4. Clean Architecture 🏗️
|
||||||
|
- **Flow**: ViewModel → UseCase → Repository → DataSource (API/DB)
|
||||||
|
- **ViewModels**: Extend `ViewModel`, expose UI state via `StateFlow`
|
||||||
|
- **UseCases**: Contain business logic, single responsibility
|
||||||
|
- **Repositories**: Abstract data sources
|
||||||
|
- **DI**: Dependencies injected (Dagger/Hilt/Koin)
|
||||||
|
- **Layers**: Strict separation (Presentation/Domain/Data)
|
||||||
|
|
||||||
|
#### 5. Lifecycle Management 🔁
|
||||||
|
- **ViewModels**: Don't hold Activity/Fragment references
|
||||||
|
- **Observers**: Use `viewLifecycleOwner` in Fragments
|
||||||
|
- **Coroutines**: Launch in lifecycle-aware scopes
|
||||||
|
- **Resources**: Clean up in `onDestroy` or `onCleared`
|
||||||
|
- **Configuration Changes**: Handle properly
|
||||||
|
|
||||||
|
#### 6. Security 🔒
|
||||||
|
- **Sensitive Data**: Use EncryptedSharedPreferences
|
||||||
|
- **API Keys**: Never hardcode, use BuildConfig
|
||||||
|
- **Network**: HTTPS only, certificate pinning if needed
|
||||||
|
- **Logs**: No sensitive data in production logs
|
||||||
|
- **Input Validation**: Sanitize user inputs
|
||||||
|
- **ProGuard/R8**: Proper obfuscation rules
|
||||||
|
|
||||||
|
#### 7. Performance ⚡
|
||||||
|
- **Background Work**: Network and DB on IO dispatcher
|
||||||
|
- **Memory Leaks**: No Activity/Context leaks
|
||||||
|
- **RecyclerView**: Use DiffUtil, ViewBinding
|
||||||
|
- **Images**: Use Coil/Glide with proper caching
|
||||||
|
- **Database**: Room queries optimized with indexes
|
||||||
|
- **Lazy Loading**: Load data on demand
|
||||||
|
|
||||||
|
### Step 4: Generate Report
|
||||||
|
|
||||||
|
Provide structured output with:
|
||||||
|
- **Summary**: Issue counts by severity (🔴 Critical, 🟠 High, 🟡 Medium, 🟢 Low)
|
||||||
|
- **Issues by category**: Organized findings
|
||||||
|
- **Code examples**: Current vs. fixed code
|
||||||
|
- **Explanations**: Why it matters
|
||||||
|
- **Recommendations**: Prioritized actions
|
||||||
|
|
||||||
|
## Severity Levels
|
||||||
|
|
||||||
|
🔴 **Critical** - Fix immediately
|
||||||
|
- Memory leaks (Activity/Context references)
|
||||||
|
- Coroutines not cancelled → Resource leak
|
||||||
|
- Sensitive data in plain SharedPreferences
|
||||||
|
- UI updates on background thread → Crash risk
|
||||||
|
- Hardcoded API keys or secrets
|
||||||
|
|
||||||
|
🟠 **High Priority** - Fix soon
|
||||||
|
- Missing error handling in coroutines
|
||||||
|
- Wrong Dispatcher usage
|
||||||
|
- ViewModel calling repository directly (skip UseCase)
|
||||||
|
- Business logic in ViewModel/Activity
|
||||||
|
- No ProGuard rules for critical code
|
||||||
|
|
||||||
|
🟡 **Medium Priority** - Should improve
|
||||||
|
- Not using lifecycle-aware components
|
||||||
|
- Poor naming conventions
|
||||||
|
- Not using data classes for models
|
||||||
|
- Missing null safety checks
|
||||||
|
- Inefficient RecyclerView usage
|
||||||
|
|
||||||
|
🟢 **Low Priority** - Nice to have
|
||||||
|
- Inconsistent code style
|
||||||
|
- Could use more extension functions
|
||||||
|
- Documentation improvements
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Android Code Review Report
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- 🔴 Critical: X | 🟠 High: X | 🟡 Medium: X | 🟢 Low: X
|
||||||
|
- By category: Naming: X, Kotlin: X, Coroutines: X, Architecture: X, etc.
|
||||||
|
|
||||||
|
## Critical Issues
|
||||||
|
|
||||||
|
### 🔴 [Category] - [Issue Title]
|
||||||
|
**File**: `path/to/file.kt:line`
|
||||||
|
|
||||||
|
**Current**:
|
||||||
|
```kotlin
|
||||||
|
// problematic code
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
```kotlin
|
||||||
|
// corrected code
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: [Explanation of impact]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
1. Fix all critical issues immediately
|
||||||
|
2. Address high priority before next release
|
||||||
|
3. Plan medium priority for next sprint
|
||||||
|
|
||||||
|
## Positive Observations
|
||||||
|
✅ [Acknowledge well-written code]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Standards**: See `standards.md` in this skill directory for detailed Android coding standards including:
|
||||||
|
- Kotlin best practices and idioms
|
||||||
|
- Coroutines patterns and anti-patterns
|
||||||
|
- Clean Architecture implementation
|
||||||
|
- Dependency injection patterns
|
||||||
|
- Security guidelines
|
||||||
|
- Performance optimization techniques
|
||||||
|
|
||||||
|
**Examples**: See `examples.md` for extensive code examples, common issues, and recommended patterns.
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- **Be thorough**: Check all 7 categories
|
||||||
|
- **Be specific**: Reference exact line numbers
|
||||||
|
- **Be constructive**: Explain why, not just what
|
||||||
|
- **Be practical**: Prioritize by severity
|
||||||
|
- **Be encouraging**: Acknowledge good code
|
||||||
|
- **Context matters**: Consider app-specific requirements
|
||||||
845
skills/android-code-review/examples.md
Normal file
845
skills/android-code-review/examples.md
Normal file
@@ -0,0 +1,845 @@
|
|||||||
|
# Android Code Review Examples
|
||||||
|
|
||||||
|
Real-world examples of common issues and recommended patterns for Android Kotlin development.
|
||||||
|
|
||||||
|
## Example 1: Memory Leak - Activity Reference in ViewModel
|
||||||
|
|
||||||
|
### ❌ BAD: Holding Activity Reference
|
||||||
|
```kotlin
|
||||||
|
// PaymentViewModel.kt
|
||||||
|
class PaymentViewModel(
|
||||||
|
private val activity: PaymentActivity // Memory leak!
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
fun onPaymentComplete() {
|
||||||
|
activity.showSuccessDialog() // Crash if Activity destroyed!
|
||||||
|
activity.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentActivity.kt
|
||||||
|
class PaymentActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val viewModel by viewModels<PaymentViewModel> {
|
||||||
|
PaymentViewModelFactory(this) // Passing Activity reference!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
- 🔴 **Critical**: Memory leak - ViewModel outlives Activity
|
||||||
|
- 🔴 **Critical**: Crash risk if Activity destroyed but ViewModel still active
|
||||||
|
- 🟠 **High**: Tight coupling between ViewModel and View
|
||||||
|
|
||||||
|
### ✅ GOOD: Event-Based Navigation
|
||||||
|
```kotlin
|
||||||
|
// PaymentViewModel.kt
|
||||||
|
class PaymentViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val _events = MutableSharedFlow<PaymentEvent>()
|
||||||
|
val events: SharedFlow<PaymentEvent> = _events.asSharedFlow()
|
||||||
|
|
||||||
|
fun onPaymentComplete() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_events.emit(PaymentEvent.ShowSuccessDialog)
|
||||||
|
_events.emit(PaymentEvent.Finish)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class PaymentEvent {
|
||||||
|
object ShowSuccessDialog : PaymentEvent()
|
||||||
|
object Finish : PaymentEvent()
|
||||||
|
data class NavigateTo(val destination: String) : PaymentEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentActivity.kt
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class PaymentActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModel: PaymentViewModel
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.events.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
PaymentEvent.ShowSuccessDialog -> showSuccessDialog()
|
||||||
|
PaymentEvent.Finish -> finish()
|
||||||
|
is PaymentEvent.NavigateTo -> navigate(event.destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ No memory leaks - ViewModel doesn't hold Activity reference
|
||||||
|
- ✅ Lifecycle-safe - Events only processed when Activity is active
|
||||||
|
- ✅ Testable - ViewModel logic can be tested independently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 2: Coroutine Scope and Cancellation
|
||||||
|
|
||||||
|
### ❌ BAD: GlobalScope and No Cancellation
|
||||||
|
```kotlin
|
||||||
|
class PaymentViewModel : ViewModel() {
|
||||||
|
|
||||||
|
fun loadPayments() {
|
||||||
|
GlobalScope.launch { // Never cancelled!
|
||||||
|
val payments = repository.getPayments()
|
||||||
|
_payments.value = payments // Can crash if ViewModel cleared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun processPayment(request: PaymentRequest) {
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
val result = repository.processPayment(request) // Blocks UI thread!
|
||||||
|
_result.value = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
- 🔴 **Critical**: GlobalScope coroutines never cancelled - resource leak
|
||||||
|
- 🔴 **Critical**: Network/DB on Main thread - ANR risk
|
||||||
|
- 🟠 **High**: Crash risk when updating UI after ViewModel cleared
|
||||||
|
|
||||||
|
### ✅ GOOD: ViewModelScope and Proper Dispatchers
|
||||||
|
```kotlin
|
||||||
|
class PaymentViewModel(
|
||||||
|
private val repository: PaymentRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<UiState<List<Payment>>>(UiState.Loading)
|
||||||
|
val uiState: StateFlow<UiState<List<Payment>>> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun loadPayments() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.value = UiState.Loading
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Repository handles IO dispatcher internally
|
||||||
|
val payments = repository.getPayments()
|
||||||
|
_uiState.value = UiState.Success(payments)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
_uiState.value = UiState.Error("Network error: ${e.message}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.value = UiState.Error("Unexpected error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun processPayment(request: PaymentRequest) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.value = UiState.Loading
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
repository.processPayment(request)
|
||||||
|
}.onSuccess { result ->
|
||||||
|
_uiState.value = UiState.Success(result)
|
||||||
|
}.onFailure { error ->
|
||||||
|
_uiState.value = UiState.Error(error.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository with proper dispatcher
|
||||||
|
class PaymentRepositoryImpl(
|
||||||
|
private val api: PaymentApi,
|
||||||
|
private val dao: PaymentDao,
|
||||||
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
) : PaymentRepository {
|
||||||
|
|
||||||
|
override suspend fun getPayments(): List<Payment> = withContext(ioDispatcher) {
|
||||||
|
api.fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun processPayment(request: PaymentRequest): PaymentResult =
|
||||||
|
withContext(ioDispatcher) {
|
||||||
|
api.processPayment(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Automatic cancellation when ViewModel cleared
|
||||||
|
- ✅ Proper thread management - IO work on background
|
||||||
|
- ✅ Comprehensive error handling
|
||||||
|
- ✅ Single source of truth for UI state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 3: Clean Architecture Violation
|
||||||
|
|
||||||
|
### ❌ BAD: ViewModel with Business Logic and Direct API Calls
|
||||||
|
```kotlin
|
||||||
|
class PaymentViewModel(
|
||||||
|
private val api: PaymentApi // Wrong layer!
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _result = MutableLiveData<PaymentResult>()
|
||||||
|
val result: LiveData<PaymentResult> = _result
|
||||||
|
|
||||||
|
fun processPayment(amount: String, merchantId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Business logic in ViewModel - WRONG!
|
||||||
|
val amountBigDecimal = try {
|
||||||
|
BigDecimal(amount)
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
_result.value = PaymentResult.Error("Invalid amount")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amountBigDecimal < BigDecimal("1000")) {
|
||||||
|
_result.value = PaymentResult.Error("Minimum amount is 1000 VND")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val fee = amountBigDecimal * BigDecimal("0.02") // Business logic!
|
||||||
|
val total = amountBigDecimal + fee
|
||||||
|
|
||||||
|
// Calling API directly - WRONG!
|
||||||
|
try {
|
||||||
|
val response = api.processPayment(
|
||||||
|
PaymentRequest(
|
||||||
|
amount = total,
|
||||||
|
merchantId = merchantId,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_result.value = PaymentResult.Success(response)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_result.value = PaymentResult.Error(e.message ?: "Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
- 🟠 **High**: Business logic in ViewModel (validation, fee calculation)
|
||||||
|
- 🟠 **High**: ViewModel depends on Data layer (API) directly
|
||||||
|
- 🟡 **Medium**: No UseCase - business logic not reusable
|
||||||
|
- 🟡 **Medium**: Mixing concerns - validation, calculation, networking
|
||||||
|
|
||||||
|
### ✅ GOOD: Proper Clean Architecture Layers
|
||||||
|
```kotlin
|
||||||
|
// Domain Layer - UseCase
|
||||||
|
class ProcessPaymentUseCase(
|
||||||
|
private val repository: PaymentRepository,
|
||||||
|
private val validator: PaymentValidator,
|
||||||
|
private val feeCalculator: FeeCalculator
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(
|
||||||
|
amount: String,
|
||||||
|
merchantId: String
|
||||||
|
): Result<PaymentResult> = runCatching {
|
||||||
|
// Business logic in UseCase
|
||||||
|
val amountBigDecimal = validator.parseAndValidateAmount(amount)
|
||||||
|
validator.validateMerchant(merchantId)
|
||||||
|
|
||||||
|
val fee = feeCalculator.calculateFee(amountBigDecimal)
|
||||||
|
val total = amountBigDecimal + fee
|
||||||
|
|
||||||
|
val request = PaymentRequest(
|
||||||
|
amount = total,
|
||||||
|
merchantId = merchantId,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
repository.processPayment(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain Layer - Validators and Calculators
|
||||||
|
class PaymentValidator {
|
||||||
|
fun parseAndValidateAmount(amount: String): BigDecimal {
|
||||||
|
val amountBigDecimal = amount.toBigDecimalOrNull()
|
||||||
|
?: throw IllegalArgumentException("Invalid amount format")
|
||||||
|
|
||||||
|
if (amountBigDecimal < MIN_AMOUNT) {
|
||||||
|
throw IllegalArgumentException("Minimum amount is $MIN_AMOUNT VND")
|
||||||
|
}
|
||||||
|
|
||||||
|
return amountBigDecimal
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateMerchant(merchantId: String) {
|
||||||
|
if (merchantId.isBlank()) {
|
||||||
|
throw IllegalArgumentException("Invalid merchant ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val MIN_AMOUNT = BigDecimal("1000")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FeeCalculator {
|
||||||
|
fun calculateFee(amount: BigDecimal): BigDecimal {
|
||||||
|
return amount * FEE_RATE
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val FEE_RATE = BigDecimal("0.02")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presentation Layer - ViewModel
|
||||||
|
class PaymentViewModel(
|
||||||
|
private val processPaymentUseCase: ProcessPaymentUseCase
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<PaymentUiState>(PaymentUiState.Initial)
|
||||||
|
val uiState: StateFlow<PaymentUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun processPayment(amount: String, merchantId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.value = PaymentUiState.Loading
|
||||||
|
|
||||||
|
processPaymentUseCase(amount, merchantId)
|
||||||
|
.onSuccess { result ->
|
||||||
|
_uiState.value = PaymentUiState.Success(result)
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
_uiState.value = PaymentUiState.Error(error.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data Layer - Repository
|
||||||
|
class PaymentRepositoryImpl(
|
||||||
|
private val remoteDataSource: PaymentRemoteDataSource,
|
||||||
|
private val localDataSource: PaymentLocalDataSource,
|
||||||
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
) : PaymentRepository {
|
||||||
|
|
||||||
|
override suspend fun processPayment(
|
||||||
|
request: PaymentRequest
|
||||||
|
): PaymentResult = withContext(ioDispatcher) {
|
||||||
|
val result = remoteDataSource.processPayment(request)
|
||||||
|
localDataSource.savePayment(result.toEntity())
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Clear separation of concerns
|
||||||
|
- ✅ Business logic in Domain layer (UseCase)
|
||||||
|
- ✅ ViewModel only handles presentation logic
|
||||||
|
- ✅ Reusable business logic
|
||||||
|
- ✅ Testable - each layer can be tested independently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 4: Insecure Storage
|
||||||
|
|
||||||
|
### ❌ BAD: Plain SharedPreferences for Sensitive Data
|
||||||
|
```kotlin
|
||||||
|
class AuthManager(private val context: Context) {
|
||||||
|
|
||||||
|
private val prefs = context.getSharedPreferences("auth", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun saveAuthToken(token: String) {
|
||||||
|
prefs.edit {
|
||||||
|
putString("auth_token", token) // Plain text!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveUserPin(pin: String) {
|
||||||
|
prefs.edit {
|
||||||
|
putString("user_pin", pin) // Plain text!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAuthToken(): String? {
|
||||||
|
return prefs.getString("auth_token", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
- 🔴 **Critical**: Auth token stored in plain text
|
||||||
|
- 🔴 **Critical**: User PIN stored in plain text
|
||||||
|
- 🔴 **Critical**: Security vulnerability - data can be read by rooted devices
|
||||||
|
|
||||||
|
### ✅ GOOD: EncryptedSharedPreferences
|
||||||
|
```kotlin
|
||||||
|
class SecureAuthManager(private val context: Context) {
|
||||||
|
|
||||||
|
private val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val encryptedPrefs = EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
"secure_auth",
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
|
||||||
|
fun saveAuthToken(token: String) {
|
||||||
|
encryptedPrefs.edit {
|
||||||
|
putString(KEY_AUTH_TOKEN, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveUserPin(pin: String) {
|
||||||
|
encryptedPrefs.edit {
|
||||||
|
putString(KEY_USER_PIN, pin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAuthToken(): String? {
|
||||||
|
return encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAuthData() {
|
||||||
|
encryptedPrefs.edit {
|
||||||
|
remove(KEY_AUTH_TOKEN)
|
||||||
|
remove(KEY_USER_PIN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_AUTH_TOKEN = "auth_token"
|
||||||
|
private const val KEY_USER_PIN = "user_pin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Encrypted storage using AES256
|
||||||
|
- ✅ Protected against unauthorized access
|
||||||
|
- ✅ Secure even on rooted devices
|
||||||
|
- ✅ Clear API for data management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 5: Inefficient RecyclerView
|
||||||
|
|
||||||
|
### ❌ BAD: notifyDataSetChanged and No ViewBinding
|
||||||
|
```kotlin
|
||||||
|
class PaymentAdapter : RecyclerView.Adapter<PaymentAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private var payments: List<Payment> = emptyList()
|
||||||
|
|
||||||
|
fun updatePayments(newPayments: List<Payment>) {
|
||||||
|
payments = newPayments
|
||||||
|
notifyDataSetChanged() // Inefficient!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_payment, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(payments[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = payments.size
|
||||||
|
|
||||||
|
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
// Finding views repeatedly - inefficient!
|
||||||
|
fun bind(payment: Payment) {
|
||||||
|
itemView.findViewById<TextView>(R.id.amountTextView).text =
|
||||||
|
payment.amount.toString()
|
||||||
|
itemView.findViewById<TextView>(R.id.merchantTextView).text =
|
||||||
|
payment.merchantName
|
||||||
|
itemView.findViewById<TextView>(R.id.dateTextView).text =
|
||||||
|
payment.formattedDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
- 🟡 **Medium**: notifyDataSetChanged() redraws entire list
|
||||||
|
- 🟡 **Medium**: findViewById called repeatedly in bind()
|
||||||
|
- 🟡 **Medium**: No ViewBinding - prone to errors
|
||||||
|
|
||||||
|
### ✅ GOOD: ListAdapter with DiffUtil and ViewBinding
|
||||||
|
```kotlin
|
||||||
|
class PaymentAdapter(
|
||||||
|
private val onItemClick: (Payment) -> Unit
|
||||||
|
) : ListAdapter<Payment, PaymentAdapter.ViewHolder>(PaymentDiffCallback()) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding = ItemPaymentBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return ViewHolder(binding, onItemClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(
|
||||||
|
private val binding: ItemPaymentBinding,
|
||||||
|
private val onItemClick: (Payment) -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(payment: Payment) {
|
||||||
|
binding.apply {
|
||||||
|
amountTextView.text = payment.amount.formatAsCurrency()
|
||||||
|
merchantTextView.text = payment.merchantName
|
||||||
|
dateTextView.text = payment.formattedDate
|
||||||
|
statusBadge.text = payment.status
|
||||||
|
|
||||||
|
// Set status color
|
||||||
|
statusBadge.setBackgroundResource(
|
||||||
|
when (payment.status) {
|
||||||
|
"SUCCESS" -> R.drawable.bg_status_success
|
||||||
|
"PENDING" -> R.drawable.bg_status_pending
|
||||||
|
"FAILED" -> R.drawable.bg_status_failed
|
||||||
|
else -> R.drawable.bg_status_unknown
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
root.setOnClickListener {
|
||||||
|
onItemClick(payment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentDiffCallback : DiffUtil.ItemCallback<Payment>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Payment, newItem: Payment): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Payment, newItem: Payment): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in Fragment
|
||||||
|
class PaymentListFragment : Fragment() {
|
||||||
|
|
||||||
|
private val adapter = PaymentAdapter { payment ->
|
||||||
|
navigateToPaymentDetail(payment.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewModel.payments.collect { payments ->
|
||||||
|
adapter.submitList(payments) // DiffUtil automatically calculates changes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Efficient updates - only changed items redrawn
|
||||||
|
- ✅ ViewBinding - type-safe, no findViewById
|
||||||
|
- ✅ Automatic animation with DiffUtil
|
||||||
|
- ✅ Click handling with lambda
|
||||||
|
- ✅ Extension function for formatting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 6: Fragment Lifecycle Issues
|
||||||
|
|
||||||
|
### ❌ BAD: Wrong Lifecycle Owner
|
||||||
|
```kotlin
|
||||||
|
class PaymentFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: FragmentPaymentBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentPaymentBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// Using fragment's lifecycle - WRONG!
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.uiState.collect { state ->
|
||||||
|
updateUi(state) // Can crash after onDestroyView!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
- 🔴 **Critical**: Using fragment lifecycle instead of view lifecycle
|
||||||
|
- 🔴 **Critical**: Crash risk - accessing binding after onDestroyView
|
||||||
|
- 🟠 **High**: Memory leak between onDestroyView and onDestroy
|
||||||
|
|
||||||
|
### ✅ GOOD: Proper Lifecycle Management
|
||||||
|
```kotlin
|
||||||
|
class PaymentFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: FragmentPaymentBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val viewModel: PaymentViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentPaymentBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
setupViews()
|
||||||
|
observeViewModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupViews() {
|
||||||
|
binding.submitButton.setOnClickListener {
|
||||||
|
val amount = binding.amountEditText.text.toString()
|
||||||
|
viewModel.processPayment(amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeViewModel() {
|
||||||
|
// Use viewLifecycleOwner for UI updates
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewModel.uiState.collect { state ->
|
||||||
|
updateUi(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate collection for one-time events
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewModel.events.collect { event ->
|
||||||
|
handleEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUi(state: PaymentUiState) {
|
||||||
|
when (state) {
|
||||||
|
is PaymentUiState.Loading -> {
|
||||||
|
binding.progressBar.show()
|
||||||
|
binding.submitButton.isEnabled = false
|
||||||
|
}
|
||||||
|
is PaymentUiState.Success -> {
|
||||||
|
binding.progressBar.hide()
|
||||||
|
binding.submitButton.isEnabled = true
|
||||||
|
showSuccessMessage(state.result)
|
||||||
|
}
|
||||||
|
is PaymentUiState.Error -> {
|
||||||
|
binding.progressBar.hide()
|
||||||
|
binding.submitButton.isEnabled = true
|
||||||
|
showError(state.message)
|
||||||
|
}
|
||||||
|
is PaymentUiState.Initial -> {
|
||||||
|
binding.progressBar.hide()
|
||||||
|
binding.submitButton.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEvent(event: PaymentEvent) {
|
||||||
|
when (event) {
|
||||||
|
is PaymentEvent.NavigateToReceipt -> {
|
||||||
|
findNavController().navigate(
|
||||||
|
PaymentFragmentDirections.actionToReceipt(event.transactionId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PaymentEvent.ShowError -> {
|
||||||
|
Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Correct lifecycle owner - viewLifecycleOwner
|
||||||
|
- ✅ No crash risk - collections cancelled when view destroyed
|
||||||
|
- ✅ Proper binding cleanup
|
||||||
|
- ✅ Separation of state and events
|
||||||
|
- ✅ Clean code organization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 7: Hardcoded Configuration
|
||||||
|
|
||||||
|
### ❌ BAD: Hardcoded API Keys and URLs
|
||||||
|
```kotlin
|
||||||
|
class ApiClient {
|
||||||
|
|
||||||
|
private val retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl("https://api.payoo.vn/v1/") // Hardcoded!
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val apiKey = "sk_live_abc123xyz789" // Hardcoded secret!
|
||||||
|
|
||||||
|
fun getPaymentApi(): PaymentApi {
|
||||||
|
return retrofit.create(PaymentApi::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthInterceptor : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
.addHeader("API-Key", "sk_live_abc123xyz789") // Hardcoded!
|
||||||
|
.build()
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
- 🔴 **Critical**: API key exposed in source code
|
||||||
|
- 🟠 **High**: Can't switch between dev/staging/prod
|
||||||
|
- 🟠 **High**: Security risk if code is decompiled
|
||||||
|
|
||||||
|
### ✅ GOOD: BuildConfig and Gradle Properties
|
||||||
|
```kotlin
|
||||||
|
// gradle.properties (add to .gitignore)
|
||||||
|
API_KEY_DEBUG=sk_test_debug_key
|
||||||
|
API_KEY_RELEASE=sk_live_production_key
|
||||||
|
API_BASE_URL_DEBUG=https://api-dev.payoo.vn/v1/
|
||||||
|
API_BASE_URL_RELEASE=https://api.payoo.vn/v1/
|
||||||
|
|
||||||
|
// app/build.gradle.kts
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY_DEBUG")}\"")
|
||||||
|
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL_DEBUG")}\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
getByName("debug") {
|
||||||
|
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY_DEBUG")}\"")
|
||||||
|
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL_DEBUG")}\"")
|
||||||
|
}
|
||||||
|
getByName("release") {
|
||||||
|
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY_RELEASE")}\"")
|
||||||
|
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL_RELEASE")}\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkModule.kt
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object NetworkModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideAuthInterceptor(): AuthInterceptor {
|
||||||
|
return AuthInterceptor(BuildConfig.API_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient {
|
||||||
|
return OkHttpClient.Builder()
|
||||||
|
.addInterceptor(authInterceptor)
|
||||||
|
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||||
|
level = if (BuildConfig.DEBUG) {
|
||||||
|
HttpLoggingInterceptor.Level.BODY
|
||||||
|
} else {
|
||||||
|
HttpLoggingInterceptor.Level.NONE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.baseUrl(BuildConfig.API_BASE_URL)
|
||||||
|
.client(okHttpClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthInterceptor(private val apiKey: String) : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
.addHeader("API-Key", apiKey)
|
||||||
|
.addHeader("Content-Type", "application/json")
|
||||||
|
.build()
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ API keys not in source code
|
||||||
|
- ✅ Different configs for debug/release
|
||||||
|
- ✅ Secure - keys in gradle.properties (gitignored)
|
||||||
|
- ✅ Easy to manage different environments
|
||||||
|
- ✅ Dependency injection for testability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary of Common Issues
|
||||||
|
|
||||||
|
### Critical (Fix Immediately)
|
||||||
|
1. Memory leaks (Activity/Context in ViewModel)
|
||||||
|
2. Coroutines not cancelled (GlobalScope)
|
||||||
|
3. Plain text storage for sensitive data
|
||||||
|
4. Hardcoded API keys/secrets
|
||||||
|
5. UI updates on background thread
|
||||||
|
|
||||||
|
### High Priority (Fix Soon)
|
||||||
|
6. Business logic in ViewModel
|
||||||
|
7. Direct API calls from ViewModel
|
||||||
|
8. Wrong lifecycle owner in Fragments
|
||||||
|
9. No error handling in coroutines
|
||||||
|
10. Wrong Dispatcher usage
|
||||||
|
|
||||||
|
### Medium Priority (Should Improve)
|
||||||
|
11. notifyDataSetChanged() instead of DiffUtil
|
||||||
|
12. findViewById instead of ViewBinding
|
||||||
|
13. No null safety checks
|
||||||
|
14. Poor naming conventions
|
||||||
|
15. Not using data classes
|
||||||
|
|
||||||
|
These examples provide concrete guidance for identifying and fixing common Android code issues during reviews.
|
||||||
953
skills/android-code-review/standards.md
Normal file
953
skills/android-code-review/standards.md
Normal file
@@ -0,0 +1,953 @@
|
|||||||
|
# Android Code Review Standards
|
||||||
|
|
||||||
|
Comprehensive standards for Android Kotlin development in the Payoo Android application.
|
||||||
|
|
||||||
|
## 1. Naming Conventions
|
||||||
|
|
||||||
|
### Types and Classes
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: PascalCase, descriptive
|
||||||
|
class PaymentViewModel
|
||||||
|
class TransactionRepository
|
||||||
|
interface UserDataSource
|
||||||
|
sealed class PaymentResult
|
||||||
|
|
||||||
|
// ❌ BAD: Abbreviations, unclear
|
||||||
|
class PmtVM
|
||||||
|
class TxnRepo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variables and Properties
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: camelCase, descriptive
|
||||||
|
val paymentAmount: BigDecimal
|
||||||
|
var isLoading: Boolean
|
||||||
|
private val transactionList: List<Transaction>
|
||||||
|
|
||||||
|
// ❌ BAD: Abbreviations, unclear
|
||||||
|
val amt: BigDecimal
|
||||||
|
var loading: Boolean
|
||||||
|
val txns: List<Transaction>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: UPPER_SNAKE_CASE
|
||||||
|
const val MAX_RETRY_COUNT = 3
|
||||||
|
const val API_BASE_URL = "https://api.payoo.vn"
|
||||||
|
private const val CACHE_DURATION_MS = 5000L
|
||||||
|
|
||||||
|
// ❌ BAD: Wrong case
|
||||||
|
const val maxRetryCount = 3
|
||||||
|
const val apiBaseUrl = "https://api.payoo.vn"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Boolean Variables
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Prefix with is, has, should, can
|
||||||
|
val isPaymentSuccessful: Boolean
|
||||||
|
val hasInternetConnection: Boolean
|
||||||
|
val shouldRetry: Boolean
|
||||||
|
val canProceed: Boolean
|
||||||
|
|
||||||
|
// ❌ BAD: No prefix
|
||||||
|
val paymentSuccessful: Boolean
|
||||||
|
val internetConnection: Boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
### View IDs and Binding
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Include type suffix
|
||||||
|
binding.amountEditText
|
||||||
|
binding.submitButton
|
||||||
|
binding.paymentRecyclerView
|
||||||
|
binding.errorTextView
|
||||||
|
|
||||||
|
// ❌ BAD: No type suffix
|
||||||
|
binding.amount
|
||||||
|
binding.submit
|
||||||
|
binding.payments
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Kotlin Best Practices
|
||||||
|
|
||||||
|
### Null Safety
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Safe calls and Elvis operator
|
||||||
|
val length = text?.length ?: 0
|
||||||
|
user?.name?.let { name ->
|
||||||
|
displayName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Force unwrap without checking
|
||||||
|
val length = text!!.length
|
||||||
|
displayName(user!!.name!!)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Classes
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Data classes for DTOs/Models
|
||||||
|
data class User(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val email: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PaymentRequest(
|
||||||
|
val amount: BigDecimal,
|
||||||
|
val merchantId: String,
|
||||||
|
val currency: String = "VND"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ❌ BAD: Regular class for simple data
|
||||||
|
class User {
|
||||||
|
var id: String = ""
|
||||||
|
var name: String = ""
|
||||||
|
var email: String = ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sealed Classes for State
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Sealed classes for state management
|
||||||
|
sealed class UiState<out T> {
|
||||||
|
object Loading : UiState<Nothing>()
|
||||||
|
data class Success<T>(val data: T) : UiState<T>()
|
||||||
|
data class Error(val message: String) : UiState<Nothing>()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class PaymentResult {
|
||||||
|
data class Success(val transactionId: String) : PaymentResult()
|
||||||
|
data class Failure(val error: PaymentError) : PaymentResult()
|
||||||
|
object Cancelled : PaymentResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Using enums or nullable types
|
||||||
|
enum class State { LOADING, SUCCESS, ERROR }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Immutability
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Prefer val over var
|
||||||
|
val userId = getUserId()
|
||||||
|
val configuration = Config(apiKey, baseUrl)
|
||||||
|
|
||||||
|
// ❌ BAD: Unnecessary var
|
||||||
|
var userId = getUserId() // Never changed
|
||||||
|
var configuration = Config(apiKey, baseUrl) // Never changed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extension Functions
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Extension functions for reusable utilities
|
||||||
|
fun String.isValidEmail(): Boolean {
|
||||||
|
return android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BigDecimal.formatAsCurrency(): String {
|
||||||
|
return NumberFormat.getCurrencyInstance(Locale("vi", "VN")).format(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.show() {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.hide() {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
if (email.isValidEmail()) {
|
||||||
|
binding.submitButton.show()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scope Functions
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Appropriate scope function usage
|
||||||
|
|
||||||
|
// apply: Configure object
|
||||||
|
val user = User().apply {
|
||||||
|
name = "John"
|
||||||
|
email = "john@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
// let: Null-safe operations
|
||||||
|
user?.let { u ->
|
||||||
|
saveUser(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// also: Side effects
|
||||||
|
val numbers = mutableListOf(1, 2, 3).also {
|
||||||
|
println("List created with ${it.size} elements")
|
||||||
|
}
|
||||||
|
|
||||||
|
// run: Execute block and return result
|
||||||
|
val result = repository.run {
|
||||||
|
fetchData()
|
||||||
|
processData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// with: Multiple operations on object
|
||||||
|
with(binding) {
|
||||||
|
titleTextView.text = title
|
||||||
|
descriptionTextView.text = description
|
||||||
|
imageView.load(imageUrl)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Coroutines Patterns
|
||||||
|
|
||||||
|
### Proper Scope Usage
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Use appropriate scope
|
||||||
|
class PaymentViewModel : ViewModel() {
|
||||||
|
fun loadPayments() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Automatically cancelled when ViewModel cleared
|
||||||
|
val payments = repository.getPayments()
|
||||||
|
_uiState.value = UiState.Success(payments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentFragment : Fragment() {
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
// Cancelled when view is destroyed
|
||||||
|
viewModel.uiState.collect { state ->
|
||||||
|
updateUi(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: GlobalScope or runBlocking
|
||||||
|
GlobalScope.launch { // Never cancelled
|
||||||
|
repository.getPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking { // Blocks thread
|
||||||
|
repository.getPayments()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dispatcher Usage
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Appropriate dispatcher for task
|
||||||
|
class PaymentRepository(
|
||||||
|
private val api: PaymentApi,
|
||||||
|
private val database: PaymentDao,
|
||||||
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
) {
|
||||||
|
suspend fun getPayments(): List<Payment> = withContext(ioDispatcher) {
|
||||||
|
val remotePayments = api.fetchPayments()
|
||||||
|
database.insertAll(remotePayments)
|
||||||
|
database.getAllPayments()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Wrong dispatcher
|
||||||
|
class PaymentRepository {
|
||||||
|
suspend fun getPayments(): List<Payment> = withContext(Dispatchers.Main) {
|
||||||
|
// Network/DB work on Main thread!
|
||||||
|
api.fetchPayments()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Proper error handling
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.value = UiState.Loading
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = repository.processPayment(request)
|
||||||
|
_uiState.value = UiState.Success(result)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
_uiState.value = UiState.Error("Network error: ${e.message}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.value = UiState.Error("Unexpected error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or using runCatching
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.value = UiState.Loading
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
repository.processPayment(request)
|
||||||
|
}.onSuccess { result ->
|
||||||
|
_uiState.value = UiState.Success(result)
|
||||||
|
}.onFailure { error ->
|
||||||
|
_uiState.value = UiState.Error(error.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: No error handling
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = repository.processPayment(request) // Can crash
|
||||||
|
_uiState.value = UiState.Success(result)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flow Usage
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: StateFlow for UI state
|
||||||
|
class PaymentViewModel : ViewModel() {
|
||||||
|
private val _uiState = MutableStateFlow<UiState<Payment>>(UiState.Loading)
|
||||||
|
val uiState: StateFlow<UiState<Payment>> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _events = MutableSharedFlow<PaymentEvent>()
|
||||||
|
val events: SharedFlow<PaymentEvent> = _events.asSharedFlow()
|
||||||
|
|
||||||
|
fun processPayment(request: PaymentRequest) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
repository.processPayment(request)
|
||||||
|
.catch { error ->
|
||||||
|
_uiState.value = UiState.Error(error.message)
|
||||||
|
}
|
||||||
|
.collect { result ->
|
||||||
|
_uiState.value = UiState.Success(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: LiveData with nullable types
|
||||||
|
class PaymentViewModel : ViewModel() {
|
||||||
|
val payment: MutableLiveData<Payment?> = MutableLiveData(null)
|
||||||
|
val error: MutableLiveData<String?> = MutableLiveData(null)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Clean Architecture
|
||||||
|
|
||||||
|
### Layer Structure
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── data/
|
||||||
|
│ ├── datasource/ # API, Database, Cache
|
||||||
|
│ ├── repository/ # Implementation
|
||||||
|
│ └── model/ # DTOs, Entities
|
||||||
|
├── domain/
|
||||||
|
│ ├── model/ # Domain models
|
||||||
|
│ ├── repository/ # Repository interfaces
|
||||||
|
│ └── usecase/ # Business logic
|
||||||
|
└── presentation/
|
||||||
|
├── ui/ # Activities, Fragments
|
||||||
|
├── viewmodel/ # ViewModels
|
||||||
|
└── adapter/ # RecyclerView adapters
|
||||||
|
```
|
||||||
|
|
||||||
|
### ViewModel (Presentation Layer)
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: ViewModel uses UseCase, exposes UI state
|
||||||
|
class PaymentViewModel(
|
||||||
|
private val processPaymentUseCase: ProcessPaymentUseCase,
|
||||||
|
private val getPaymentHistoryUseCase: GetPaymentHistoryUseCase
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<PaymentUiState>(PaymentUiState.Initial)
|
||||||
|
val uiState: StateFlow<PaymentUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun processPayment(amount: BigDecimal, merchantId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.value = PaymentUiState.Loading
|
||||||
|
|
||||||
|
processPaymentUseCase(amount, merchantId)
|
||||||
|
.onSuccess { result ->
|
||||||
|
_uiState.value = PaymentUiState.Success(result)
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
_uiState.value = PaymentUiState.Error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: ViewModel calls repository directly, has business logic
|
||||||
|
class PaymentViewModel(
|
||||||
|
private val repository: PaymentRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
fun processPayment(amount: BigDecimal, merchantId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Business logic in ViewModel - WRONG!
|
||||||
|
if (amount < BigDecimal.ZERO) {
|
||||||
|
_error.value = "Invalid amount"
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val fee = amount * BigDecimal("0.02") // Business logic!
|
||||||
|
val total = amount + fee
|
||||||
|
|
||||||
|
// Calling repository directly - WRONG!
|
||||||
|
repository.processPayment(total, merchantId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UseCase (Domain Layer)
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: UseCase contains business logic
|
||||||
|
class ProcessPaymentUseCase(
|
||||||
|
private val paymentRepository: PaymentRepository,
|
||||||
|
private val feeCalculator: FeeCalculator,
|
||||||
|
private val validator: PaymentValidator
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(
|
||||||
|
amount: BigDecimal,
|
||||||
|
merchantId: String
|
||||||
|
): Result<PaymentResult> = runCatching {
|
||||||
|
// Business logic here
|
||||||
|
validator.validateAmount(amount)
|
||||||
|
validator.validateMerchant(merchantId)
|
||||||
|
|
||||||
|
val fee = feeCalculator.calculateFee(amount)
|
||||||
|
val totalAmount = amount + fee
|
||||||
|
|
||||||
|
val request = PaymentRequest(
|
||||||
|
amount = totalAmount,
|
||||||
|
merchantId = merchantId,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
paymentRepository.processPayment(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: UseCase just forwards to repository
|
||||||
|
class ProcessPaymentUseCase(
|
||||||
|
private val repository: PaymentRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(request: PaymentRequest) =
|
||||||
|
repository.processPayment(request) // No value added!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repository (Data Layer)
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Repository abstracts data sources
|
||||||
|
interface PaymentRepository {
|
||||||
|
suspend fun processPayment(request: PaymentRequest): PaymentResult
|
||||||
|
suspend fun getPaymentHistory(): List<Payment>
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentRepositoryImpl(
|
||||||
|
private val remoteDataSource: PaymentRemoteDataSource,
|
||||||
|
private val localDataSource: PaymentLocalDataSource,
|
||||||
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
) : PaymentRepository {
|
||||||
|
|
||||||
|
override suspend fun processPayment(
|
||||||
|
request: PaymentRequest
|
||||||
|
): PaymentResult = withContext(ioDispatcher) {
|
||||||
|
val result = remoteDataSource.processPayment(request)
|
||||||
|
localDataSource.savePayment(result.toEntity())
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPaymentHistory(): List<Payment> = withContext(ioDispatcher) {
|
||||||
|
try {
|
||||||
|
val remote = remoteDataSource.getPayments()
|
||||||
|
localDataSource.saveAll(remote.map { it.toEntity() })
|
||||||
|
remote
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// Fallback to cache
|
||||||
|
localDataSource.getPayments().map { it.toDomain() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Repository has business logic
|
||||||
|
class PaymentRepositoryImpl(
|
||||||
|
private val api: PaymentApi
|
||||||
|
) : PaymentRepository {
|
||||||
|
|
||||||
|
override suspend fun processPayment(request: PaymentRequest): PaymentResult {
|
||||||
|
// Business logic in repository - WRONG!
|
||||||
|
if (request.amount < MIN_AMOUNT) {
|
||||||
|
throw IllegalArgumentException("Amount too low")
|
||||||
|
}
|
||||||
|
|
||||||
|
val fee = request.amount * FEE_RATE // Business logic!
|
||||||
|
val modifiedRequest = request.copy(amount = request.amount + fee)
|
||||||
|
|
||||||
|
return api.processPayment(modifiedRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Lifecycle Management
|
||||||
|
|
||||||
|
### Activity/Fragment References
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: No Activity/Fragment references in ViewModel
|
||||||
|
class PaymentViewModel(
|
||||||
|
private val processPaymentUseCase: ProcessPaymentUseCase
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
|
||||||
|
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent.asSharedFlow()
|
||||||
|
|
||||||
|
fun onPaymentSuccess() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_navigationEvent.emit(NavigationEvent.ToReceipt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In Fragment
|
||||||
|
class PaymentFragment : Fragment() {
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewModel.navigationEvent.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
NavigationEvent.ToReceipt -> navigateToReceipt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Holding Activity/Fragment reference
|
||||||
|
class PaymentViewModel(
|
||||||
|
private val fragment: PaymentFragment // Memory leak!
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
fun onPaymentSuccess() {
|
||||||
|
fragment.navigateToReceipt() // Crash if Fragment destroyed!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Observer Lifecycle
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Use viewLifecycleOwner in Fragments
|
||||||
|
class PaymentFragment : Fragment() {
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// Use viewLifecycleOwner, not this (fragment lifecycle)
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewModel.uiState.collect { state ->
|
||||||
|
updateUi(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Using fragment's lifecycle
|
||||||
|
class PaymentFragment : Fragment() {
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// This leaks between onDestroyView and onDestroy!
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.uiState.collect { state ->
|
||||||
|
updateUi(state) // Can crash if view is destroyed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Proper cleanup
|
||||||
|
class PaymentViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: No cleanup
|
||||||
|
class PaymentViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
// No cleanup - scope keeps running!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Security Best Practices
|
||||||
|
|
||||||
|
### Secure Storage
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Encrypted storage for sensitive data
|
||||||
|
class SecurePreferences(context: Context) {
|
||||||
|
|
||||||
|
private val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val encryptedPrefs = EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
"secure_prefs",
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
|
||||||
|
fun saveToken(token: String) {
|
||||||
|
encryptedPrefs.edit {
|
||||||
|
putString(KEY_AUTH_TOKEN, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Plain SharedPreferences for sensitive data
|
||||||
|
class Preferences(context: Context) {
|
||||||
|
|
||||||
|
private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun saveToken(token: String) {
|
||||||
|
prefs.edit {
|
||||||
|
putString("auth_token", token) // Plain text!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Keys
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: API keys in gradle.properties (not checked in) and BuildConfig
|
||||||
|
// gradle.properties (add to .gitignore)
|
||||||
|
API_KEY=your_secret_key_here
|
||||||
|
API_BASE_URL=https://api.payoo.vn
|
||||||
|
|
||||||
|
// build.gradle.kts
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY")}\"")
|
||||||
|
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL")}\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
class ApiClient {
|
||||||
|
private val apiKey = BuildConfig.API_KEY
|
||||||
|
private val baseUrl = BuildConfig.API_BASE_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Hardcoded API keys
|
||||||
|
class ApiClient {
|
||||||
|
private val apiKey = "sk_live_1234567890abcdef" // Exposed in code!
|
||||||
|
private val baseUrl = "https://api.payoo.vn"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: No sensitive data in logs, conditional logging
|
||||||
|
class PaymentLogger {
|
||||||
|
|
||||||
|
fun logPayment(request: PaymentRequest) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(TAG, "Processing payment for merchant: ${request.merchantId}")
|
||||||
|
Log.d(TAG, "Amount: ${request.amount.setScale(2)}")
|
||||||
|
// NO card numbers, tokens, or PII
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Logging sensitive data
|
||||||
|
class PaymentLogger {
|
||||||
|
|
||||||
|
fun logPayment(request: PaymentRequest) {
|
||||||
|
Log.d(TAG, "Payment: $request") // Might contain sensitive data!
|
||||||
|
Log.d(TAG, "Token: ${request.authToken}") // Exposed in logs!
|
||||||
|
Log.d(TAG, "Card: ${request.cardNumber}") // PCI violation!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Performance Optimization
|
||||||
|
|
||||||
|
### Background Work
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Network/DB on background thread
|
||||||
|
class PaymentRepository(
|
||||||
|
private val api: PaymentApi,
|
||||||
|
private val dao: PaymentDao,
|
||||||
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun syncPayments() = withContext(ioDispatcher) {
|
||||||
|
val remotePayments = api.fetchPayments()
|
||||||
|
dao.deleteAll()
|
||||||
|
dao.insertAll(remotePayments.map { it.toEntity() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Blocking Main thread
|
||||||
|
class PaymentRepository(
|
||||||
|
private val api: PaymentApi,
|
||||||
|
private val dao: PaymentDao
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun syncPayments() {
|
||||||
|
// Blocking Main thread!
|
||||||
|
val remotePayments = api.fetchPayments().execute()
|
||||||
|
dao.deleteAll()
|
||||||
|
dao.insertAll(remotePayments.map { it.toEntity() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### RecyclerView Optimization
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: DiffUtil for efficient updates
|
||||||
|
class PaymentAdapter : ListAdapter<Payment, PaymentViewHolder>(PaymentDiffCallback()) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentViewHolder {
|
||||||
|
val binding = ItemPaymentBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return PaymentViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: PaymentViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentDiffCallback : DiffUtil.ItemCallback<Payment>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Payment, newItem: Payment) =
|
||||||
|
oldItem.id == newItem.id
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Payment, newItem: Payment) =
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: notifyDataSetChanged for all updates
|
||||||
|
class PaymentAdapter : RecyclerView.Adapter<PaymentViewHolder>() {
|
||||||
|
|
||||||
|
private var payments: List<Payment> = emptyList()
|
||||||
|
|
||||||
|
fun updatePayments(newPayments: List<Payment>) {
|
||||||
|
payments = newPayments
|
||||||
|
notifyDataSetChanged() // Inefficient!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image Loading
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Coil/Glide with proper caching
|
||||||
|
class ImageLoader(private val context: Context) {
|
||||||
|
|
||||||
|
fun loadImage(imageView: ImageView, url: String) {
|
||||||
|
imageView.load(url) {
|
||||||
|
crossfade(true)
|
||||||
|
placeholder(R.drawable.placeholder)
|
||||||
|
error(R.drawable.error)
|
||||||
|
transformations(CircleCropTransformation())
|
||||||
|
memoryCachePolicy(CachePolicy.ENABLED)
|
||||||
|
diskCachePolicy(CachePolicy.ENABLED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Loading images without caching
|
||||||
|
class ImageLoader {
|
||||||
|
|
||||||
|
suspend fun loadImage(imageView: ImageView, url: String) {
|
||||||
|
val bitmap = withContext(Dispatchers.IO) {
|
||||||
|
URL(url).openStream().use { stream ->
|
||||||
|
BitmapFactory.decodeStream(stream) // No caching, inefficient!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageView.setImageBitmap(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Optimization
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Indexed queries, efficient types
|
||||||
|
@Entity(
|
||||||
|
tableName = "payments",
|
||||||
|
indices = [
|
||||||
|
Index(value = ["merchantId"]),
|
||||||
|
Index(value = ["timestamp"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class PaymentEntity(
|
||||||
|
@PrimaryKey val id: String,
|
||||||
|
val merchantId: String,
|
||||||
|
val amount: Long, // Store cents, not BigDecimal
|
||||||
|
val timestamp: Long,
|
||||||
|
val status: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PaymentDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM payments WHERE merchantId = :merchantId AND timestamp > :after ORDER BY timestamp DESC")
|
||||||
|
fun getPaymentsByMerchant(merchantId: String, after: Long): Flow<List<PaymentEntity>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertAll(payments: List<PaymentEntity>)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: No indexes, inefficient queries
|
||||||
|
@Entity(tableName = "payments")
|
||||||
|
data class PaymentEntity(
|
||||||
|
@PrimaryKey val id: String,
|
||||||
|
val merchantId: String,
|
||||||
|
val amount: BigDecimal, // Not supported by Room!
|
||||||
|
val timestamp: Long,
|
||||||
|
val status: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PaymentDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM payments")
|
||||||
|
fun getAllPayments(): List<PaymentEntity> // Load all, then filter!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Dependency Injection
|
||||||
|
|
||||||
|
### Hilt/Dagger Setup
|
||||||
|
```kotlin
|
||||||
|
// ✅ GOOD: Proper DI with Hilt
|
||||||
|
@HiltAndroidApp
|
||||||
|
class PayooApplication : Application()
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object NetworkModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideOkHttpClient(): OkHttpClient {
|
||||||
|
return OkHttpClient.Builder()
|
||||||
|
.addInterceptor(AuthInterceptor())
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.baseUrl(BuildConfig.API_BASE_URL)
|
||||||
|
.client(okHttpClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providePaymentApi(retrofit: Retrofit): PaymentApi {
|
||||||
|
return retrofit.create(PaymentApi::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
abstract class RepositoryModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindPaymentRepository(
|
||||||
|
impl: PaymentRepositoryImpl
|
||||||
|
): PaymentRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class PaymentActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModelFactory: PaymentViewModelFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Manual instantiation, singletons
|
||||||
|
object ApiClient {
|
||||||
|
val instance: PaymentApi by lazy {
|
||||||
|
Retrofit.Builder()
|
||||||
|
.baseUrl("https://api.payoo.vn")
|
||||||
|
.build()
|
||||||
|
.create(PaymentApi::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentViewModel {
|
||||||
|
private val repository = PaymentRepository(ApiClient.instance) // Tight coupling!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary Checklist
|
||||||
|
|
||||||
|
Use this checklist during code reviews:
|
||||||
|
|
||||||
|
### Naming ✅
|
||||||
|
- [ ] Classes/Interfaces in PascalCase
|
||||||
|
- [ ] Variables/Functions in camelCase
|
||||||
|
- [ ] Constants in UPPER_SNAKE_CASE
|
||||||
|
- [ ] Boolean variables prefixed (is, has, should, can)
|
||||||
|
- [ ] No unclear abbreviations
|
||||||
|
|
||||||
|
### Kotlin ✅
|
||||||
|
- [ ] Null safety properly handled
|
||||||
|
- [ ] Data classes for DTOs/models
|
||||||
|
- [ ] Sealed classes for states
|
||||||
|
- [ ] Prefer val over var
|
||||||
|
- [ ] Extension functions where appropriate
|
||||||
|
|
||||||
|
### Coroutines ✅
|
||||||
|
- [ ] Proper scope usage (viewModelScope, lifecycleScope)
|
||||||
|
- [ ] Correct dispatchers (IO for network/DB, Main for UI)
|
||||||
|
- [ ] Error handling in all coroutines
|
||||||
|
- [ ] Flows for reactive data
|
||||||
|
- [ ] No runBlocking in production
|
||||||
|
|
||||||
|
### Architecture ✅
|
||||||
|
- [ ] Clear layer separation (Presentation/Domain/Data)
|
||||||
|
- [ ] ViewModels use UseCases
|
||||||
|
- [ ] UseCases contain business logic
|
||||||
|
- [ ] Repositories abstract data sources
|
||||||
|
- [ ] Dependency injection configured
|
||||||
|
|
||||||
|
### Lifecycle ✅
|
||||||
|
- [ ] No Activity/Fragment references in ViewModel
|
||||||
|
- [ ] viewLifecycleOwner in Fragments
|
||||||
|
- [ ] Proper cleanup in onCleared
|
||||||
|
- [ ] Configuration changes handled
|
||||||
|
|
||||||
|
### Security ✅
|
||||||
|
- [ ] EncryptedSharedPreferences for sensitive data
|
||||||
|
- [ ] No hardcoded API keys/secrets
|
||||||
|
- [ ] No sensitive data in logs
|
||||||
|
- [ ] HTTPS only
|
||||||
|
- [ ] Input validation
|
||||||
|
|
||||||
|
### Performance ✅
|
||||||
|
- [ ] Network/DB on background threads
|
||||||
|
- [ ] No memory leaks
|
||||||
|
- [ ] DiffUtil in RecyclerView
|
||||||
|
- [ ] Image caching (Coil/Glide)
|
||||||
|
- [ ] Database queries optimized
|
||||||
|
|
||||||
|
This comprehensive standards document provides detailed guidance for Android code reviews in the Payoo Android application.
|
||||||
181
skills/clean-architecture-review/SKILL.md
Normal file
181
skills/clean-architecture-review/SKILL.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
---
|
||||||
|
name: clean-architecture-review
|
||||||
|
description: Validate Clean Architecture implementation in iOS. Checks layer separation (Presentation/Domain/Data), MVVM patterns, dependency injection with Swinject, and UseCase/Repository patterns. Use when reviewing architecture, checking layer boundaries, or validating DI.
|
||||||
|
allowed-tools: Read, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# Clean Architecture Validator
|
||||||
|
|
||||||
|
Verify Clean Architecture and MVVM implementation in iOS code following Payoo Merchant patterns.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- "architecture review", "layer separation", "clean architecture"
|
||||||
|
- "MVVM", "dependency injection", "DI"
|
||||||
|
- "use case", "repository pattern"
|
||||||
|
- Reviewing module structure or refactoring
|
||||||
|
|
||||||
|
## Architecture Layers
|
||||||
|
|
||||||
|
**Presentation** → ViewControllers, ViewModels, Views
|
||||||
|
**Domain** → UseCases (business logic), Models, Repository protocols
|
||||||
|
**Data** → Repository implementations, API Services, Local Storage
|
||||||
|
|
||||||
|
**Correct Flow**:
|
||||||
|
```
|
||||||
|
UI → ViewController → ViewModel → UseCase → Repository → API/DB
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Process
|
||||||
|
|
||||||
|
### Step 1: Map Architecture
|
||||||
|
|
||||||
|
Classify files into layers:
|
||||||
|
- Presentation: `*ViewController.swift`, `*ViewModel.swift`
|
||||||
|
- Domain: `*UseCase.swift`, `*Repository.swift` (protocols)
|
||||||
|
- Data: `*RepositoryImpl.swift`, `*ApiService.swift`
|
||||||
|
|
||||||
|
### Step 2: Check Layer Violations
|
||||||
|
|
||||||
|
**Critical Issues**:
|
||||||
|
- 🔴 ViewModel calling API directly (bypassing UseCase)
|
||||||
|
- 🔴 Business logic in ViewModel (should be in UseCase)
|
||||||
|
- 🔴 UseCase calling API directly (bypassing Repository)
|
||||||
|
- 🔴 Direct instantiation (no DI)
|
||||||
|
|
||||||
|
### Step 3: Verify Patterns
|
||||||
|
|
||||||
|
**BaseViewModel**:
|
||||||
|
```swift
|
||||||
|
✅ class PaymentViewModel: BaseViewModel<PaymentState>
|
||||||
|
❌ class PaymentViewModel // Should extend BaseViewModel
|
||||||
|
```
|
||||||
|
|
||||||
|
**UseCase Pattern**:
|
||||||
|
```swift
|
||||||
|
✅ protocol PaymentUseCase { }
|
||||||
|
✅ class PaymentUseCaseImpl: PaymentUseCase { }
|
||||||
|
❌ class PaymentUseCase { } // Should be protocol + impl
|
||||||
|
```
|
||||||
|
|
||||||
|
**Repository Pattern**:
|
||||||
|
```swift
|
||||||
|
✅ protocol PaymentRepository { } // In Domain
|
||||||
|
✅ class PaymentRepositoryImpl: PaymentRepository { } // In Data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dependency Injection**:
|
||||||
|
```swift
|
||||||
|
✅ init(paymentUC: PaymentUseCase) { // Constructor injection
|
||||||
|
self.paymentUC = paymentUC
|
||||||
|
}
|
||||||
|
❌ let paymentUC = PaymentUseCaseImpl() // Direct instantiation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Generate Report
|
||||||
|
|
||||||
|
Provide:
|
||||||
|
- Architecture compliance score
|
||||||
|
- Layer violations by severity
|
||||||
|
- Current vs. should-be architecture
|
||||||
|
- Refactoring steps
|
||||||
|
- Effort estimate
|
||||||
|
|
||||||
|
## Common Violations
|
||||||
|
|
||||||
|
### ❌ ViewModel Bypassing UseCase
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel {
|
||||||
|
private let apiService: PaymentApiService // WRONG LAYER!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Should be**:
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel {
|
||||||
|
private let paymentUC: PaymentUseCase // CORRECT!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Business Logic in ViewModel
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel {
|
||||||
|
func processPayment(amount: Double) {
|
||||||
|
// ❌ Validation in ViewModel
|
||||||
|
guard amount > 1000 else { return }
|
||||||
|
// ❌ Business rules in ViewModel
|
||||||
|
let fee = amount * 0.01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Should be in UseCase**:
|
||||||
|
```swift
|
||||||
|
class PaymentUseCaseImpl {
|
||||||
|
func execute(amount: Double) -> Single<PaymentResult> {
|
||||||
|
// ✅ Validation in UseCase
|
||||||
|
return validateAmount(amount)
|
||||||
|
.flatMap { processPayment($0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Clean Architecture Review
|
||||||
|
|
||||||
|
## Compliance Score: X/100
|
||||||
|
|
||||||
|
## Critical Violations: X
|
||||||
|
|
||||||
|
### 1. ViewModel Bypassing UseCase
|
||||||
|
**File**: `PaymentViewModel.swift:15`
|
||||||
|
**Current**: ViewModel → API
|
||||||
|
**Should be**: ViewModel → UseCase → Repository → API
|
||||||
|
|
||||||
|
**Fix**: [Refactoring steps]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Graph
|
||||||
|
|
||||||
|
### Current (Problematic)
|
||||||
|
ViewModel → ApiService ❌
|
||||||
|
|
||||||
|
### Should Be
|
||||||
|
ViewModel → UseCase → Repository → ApiService ✅
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
1. Create missing UseCases
|
||||||
|
2. Move business logic to Domain layer
|
||||||
|
3. Setup DI container
|
||||||
|
4. Add Repository layer
|
||||||
|
|
||||||
|
## Effort Estimate
|
||||||
|
- Module refactoring: X hours
|
||||||
|
- DI setup: X hours
|
||||||
|
- Testing: X hours
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Checks
|
||||||
|
|
||||||
|
**Layer Boundaries**:
|
||||||
|
- [ ] ViewModels only depend on UseCases
|
||||||
|
- [ ] UseCases contain all business logic
|
||||||
|
- [ ] Repositories handle data access only
|
||||||
|
- [ ] No UI code in Domain/Data layers
|
||||||
|
|
||||||
|
**Dependency Injection**:
|
||||||
|
- [ ] All dependencies via constructor
|
||||||
|
- [ ] No direct instantiation
|
||||||
|
- [ ] Swinject container registration
|
||||||
|
- [ ] Protocol-based dependencies
|
||||||
|
|
||||||
|
**Patterns**:
|
||||||
|
- [ ] ViewModels extend BaseViewModel
|
||||||
|
- [ ] UseCases follow protocol + impl
|
||||||
|
- [ ] Repositories follow protocol + impl
|
||||||
|
- [ ] State management via setState()
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
**Detailed Examples**: See `examples.md` for complete architecture patterns and refactoring guides.
|
||||||
505
skills/clean-architecture-review/examples.md
Normal file
505
skills/clean-architecture-review/examples.md
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
# Clean Architecture Examples
|
||||||
|
|
||||||
|
Complete examples of proper layer separation, MVVM patterns, and dependency injection.
|
||||||
|
|
||||||
|
## Complete Architecture Example
|
||||||
|
|
||||||
|
### Proper 3-Layer Implementation
|
||||||
|
|
||||||
|
#### Presentation Layer - ViewModel
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
// ✅ Depends only on UseCase (Domain layer)
|
||||||
|
private let paymentUC: PaymentUseCase
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
let paymentAmount = BehaviorRelay<String>(value: "")
|
||||||
|
let isProcessing = BehaviorRelay<Bool>(value: false)
|
||||||
|
|
||||||
|
// ✅ Constructor injection
|
||||||
|
init(paymentUC: PaymentUseCase) {
|
||||||
|
self.paymentUC = paymentUC
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func processPayment() {
|
||||||
|
isProcessing.accept(true)
|
||||||
|
|
||||||
|
// ✅ Delegates to UseCase, no business logic here
|
||||||
|
paymentUC.execute(amount: paymentAmount.value)
|
||||||
|
.subscribeOn(ConcurrentScheduler.background)
|
||||||
|
.observeOn(MainScheduler.instance)
|
||||||
|
.subscribe(
|
||||||
|
onNext: { [weak self] result in
|
||||||
|
self?.setState(.success(result))
|
||||||
|
},
|
||||||
|
onError: { [weak self] error in
|
||||||
|
self?.setState(.error(error))
|
||||||
|
},
|
||||||
|
onCompleted: { [weak self] in
|
||||||
|
self?.isProcessing.accept(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Domain Layer - UseCase
|
||||||
|
```swift
|
||||||
|
// ✅ Protocol in Domain layer
|
||||||
|
protocol PaymentUseCase {
|
||||||
|
func execute(amount: String) -> Single<PaymentResult>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Implementation in Domain layer
|
||||||
|
class PaymentUseCaseImpl: PaymentUseCase {
|
||||||
|
// ✅ Depends on Repository protocol (Domain layer)
|
||||||
|
private let paymentRepository: PaymentRepository
|
||||||
|
private let validationService: ValidationService
|
||||||
|
|
||||||
|
init(paymentRepository: PaymentRepository,
|
||||||
|
validationService: ValidationService) {
|
||||||
|
self.paymentRepository = paymentRepository
|
||||||
|
self.validationService = validationService
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(amount: String) -> Single<PaymentResult> {
|
||||||
|
// ✅ Business logic in UseCase
|
||||||
|
return validationService.validateAmount(amount)
|
||||||
|
.flatMap { validatedAmount in
|
||||||
|
// ✅ Calls Repository, not API directly
|
||||||
|
return self.paymentRepository.processPayment(amount: validatedAmount)
|
||||||
|
}
|
||||||
|
.map { response in
|
||||||
|
// ✅ Business rules applied here
|
||||||
|
return self.applyBusinessRules(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyBusinessRules(_ response: PaymentResponse) -> PaymentResult {
|
||||||
|
// Business logic here
|
||||||
|
return PaymentResult(from: response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Data Layer - Repository
|
||||||
|
```swift
|
||||||
|
// ✅ Protocol in Domain layer
|
||||||
|
protocol PaymentRepository {
|
||||||
|
func processPayment(amount: Double) -> Single<PaymentResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Implementation in Data layer
|
||||||
|
class PaymentRepositoryImpl: PaymentRepository {
|
||||||
|
private let apiService: PaymentApiService
|
||||||
|
private let localStorage: PaymentLocalStorage
|
||||||
|
|
||||||
|
init(apiService: PaymentApiService, localStorage: PaymentLocalStorage) {
|
||||||
|
self.apiService = apiService
|
||||||
|
self.localStorage = localStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
func processPayment(amount: Double) -> Single<PaymentResponse> {
|
||||||
|
// ✅ Data access only, no business logic
|
||||||
|
return apiService.processPayment(amount: amount)
|
||||||
|
.do(onSuccess: { [weak self] response in
|
||||||
|
// Save to local storage
|
||||||
|
self?.localStorage.savePaymentRecord(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DI Setup - Swinject
|
||||||
|
```swift
|
||||||
|
extension Container {
|
||||||
|
func registerPaymentModule() {
|
||||||
|
// Register Use Cases
|
||||||
|
register(PaymentUseCase.self) { resolver in
|
||||||
|
PaymentUseCaseImpl(
|
||||||
|
paymentRepository: resolver.resolve(PaymentRepository.self)!,
|
||||||
|
validationService: resolver.resolve(ValidationService.self)!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register Repositories
|
||||||
|
register(PaymentRepository.self) { resolver in
|
||||||
|
PaymentRepositoryImpl(
|
||||||
|
apiService: resolver.resolve(PaymentApiService.self)!,
|
||||||
|
localStorage: resolver.resolve(PaymentLocalStorage.self)!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register ViewModels
|
||||||
|
register(PaymentViewModel.self) { resolver in
|
||||||
|
PaymentViewModel(
|
||||||
|
paymentUC: resolver.resolve(PaymentUseCase.self)!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Anti-Patterns
|
||||||
|
|
||||||
|
### ❌ Anti-Pattern 1: ViewModel Calling API Directly
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
private let apiService: PaymentApiService // ❌ Wrong layer!
|
||||||
|
|
||||||
|
func processPayment(amount: Double) {
|
||||||
|
// ❌ Direct API call bypasses business logic
|
||||||
|
apiService.processPayment(amount: amount)
|
||||||
|
.subscribe(onNext: { result in
|
||||||
|
// Handle result
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problems**:
|
||||||
|
- Business logic scattered or missing
|
||||||
|
- Hard to test
|
||||||
|
- Violates layer separation
|
||||||
|
- Cannot reuse logic elsewhere
|
||||||
|
|
||||||
|
**Fix**: Add UseCase layer
|
||||||
|
```swift
|
||||||
|
// 1. Create UseCase
|
||||||
|
protocol PaymentUseCase {
|
||||||
|
func execute(amount: Double) -> Single<PaymentResult>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update ViewModel
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
private let paymentUC: PaymentUseCase // ✅ Correct!
|
||||||
|
|
||||||
|
func processPayment(amount: Double) {
|
||||||
|
paymentUC.execute(amount: amount) // ✅ Through UseCase
|
||||||
|
.subscribe(/* ... */)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❌ Anti-Pattern 2: Business Logic in ViewModel
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
func processPayment(amount: String) {
|
||||||
|
// ❌ Validation logic in ViewModel
|
||||||
|
guard let amt = Double(amount), amt > 1000 else {
|
||||||
|
setState(.showError("Invalid amount"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Business rules in ViewModel
|
||||||
|
let fee = amt * 0.01
|
||||||
|
let total = amt + fee
|
||||||
|
|
||||||
|
if total > 50_000_000 {
|
||||||
|
setState(.showError("Amount too high"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problems**:
|
||||||
|
- Cannot reuse validation/business logic
|
||||||
|
- Hard to test logic independently
|
||||||
|
- ViewModel becomes complex
|
||||||
|
- Violates Single Responsibility
|
||||||
|
|
||||||
|
**Fix**: Move to UseCase
|
||||||
|
```swift
|
||||||
|
// ✅ Business logic in UseCase
|
||||||
|
class PaymentUseCaseImpl: PaymentUseCase {
|
||||||
|
func execute(amount: String) -> Single<PaymentResult> {
|
||||||
|
return validateAmount(amount)
|
||||||
|
.flatMap { validatedAmount in
|
||||||
|
return self.calculateFeesAndProcess(validatedAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateAmount(_ amount: String) -> Single<Double> {
|
||||||
|
guard let amt = Double(amount), amt > 1000 else {
|
||||||
|
return .error(PaymentError.invalidAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
let fee = amt * 0.01
|
||||||
|
let total = amt + fee
|
||||||
|
|
||||||
|
guard total <= 50_000_000 else {
|
||||||
|
return .error(PaymentError.amountTooHigh)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .just(amt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ViewModel simplified
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
func processPayment(amount: String) {
|
||||||
|
paymentUC.execute(amount: amount)
|
||||||
|
.subscribe(
|
||||||
|
onNext: { [weak self] result in
|
||||||
|
self?.setState(.success(result))
|
||||||
|
},
|
||||||
|
onError: { [weak self] error in
|
||||||
|
self?.setState(.error(error))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❌ Anti-Pattern 3: UseCase Calling API Directly
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class PaymentUseCaseImpl: PaymentUseCase {
|
||||||
|
private let apiService: PaymentApiService // ❌ Bypasses Repository!
|
||||||
|
|
||||||
|
func execute(amount: Double) -> Single<PaymentResult> {
|
||||||
|
// ❌ Direct API call
|
||||||
|
return apiService.processPayment(amount: amount)
|
||||||
|
.map { response in
|
||||||
|
return PaymentResult(from: response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problems**:
|
||||||
|
- Cannot swap data sources (API/local/mock)
|
||||||
|
- Hard to test independently
|
||||||
|
- Violates Dependency Inversion
|
||||||
|
|
||||||
|
**Fix**: Add Repository layer
|
||||||
|
```swift
|
||||||
|
// ✅ Repository protocol in Domain
|
||||||
|
protocol PaymentRepository {
|
||||||
|
func processPayment(amount: Double) -> Single<PaymentResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ UseCase depends on protocol
|
||||||
|
class PaymentUseCaseImpl: PaymentUseCase {
|
||||||
|
private let paymentRepository: PaymentRepository // ✅ Correct!
|
||||||
|
|
||||||
|
func execute(amount: Double) -> Single<PaymentResult> {
|
||||||
|
return paymentRepository.processPayment(amount: amount)
|
||||||
|
.map { response in
|
||||||
|
return PaymentResult(from: response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Implementation in Data layer
|
||||||
|
class PaymentRepositoryImpl: PaymentRepository {
|
||||||
|
private let apiService: PaymentApiService
|
||||||
|
|
||||||
|
func processPayment(amount: Double) -> Single<PaymentResponse> {
|
||||||
|
return apiService.processPayment(amount: amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Refactoring Examples
|
||||||
|
|
||||||
|
### Example: Refactoring ViewModel with Business Logic
|
||||||
|
|
||||||
|
#### Before (Violates Clean Architecture)
|
||||||
|
```swift
|
||||||
|
class TransactionViewModel: BaseViewModel<TransactionState> {
|
||||||
|
private let apiService: TransactionApiService
|
||||||
|
|
||||||
|
func loadTransactions(from startDate: Date, to endDate: Date) {
|
||||||
|
// ❌ Date validation in ViewModel
|
||||||
|
guard startDate <= endDate else {
|
||||||
|
setState(.showError("Invalid date range"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Business rule in ViewModel
|
||||||
|
let daysDiff = Calendar.current.dateComponents([.day], from: startDate, to: endDate).day!
|
||||||
|
guard daysDiff <= 90 else {
|
||||||
|
setState(.showError("Date range too large"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Direct API call
|
||||||
|
apiService.getTransactions(from: startDate, to: endDate)
|
||||||
|
.subscribe(onNext: { [weak self] transactions in
|
||||||
|
// ❌ Business logic: filtering
|
||||||
|
let filtered = transactions.filter { $0.amount > 0 }
|
||||||
|
self?.transactions.accept(filtered)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After (Clean Architecture)
|
||||||
|
```swift
|
||||||
|
// DOMAIN LAYER - UseCase
|
||||||
|
protocol TransactionUseCase {
|
||||||
|
func getTransactions(from: Date, to: Date) -> Single<[Transaction]>
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransactionUseCaseImpl: TransactionUseCase {
|
||||||
|
private let repository: TransactionRepository
|
||||||
|
|
||||||
|
func getTransactions(from startDate: Date, to endDate: Date) -> Single<[Transaction]> {
|
||||||
|
// ✅ Validation in UseCase
|
||||||
|
return validateDateRange(startDate, endDate)
|
||||||
|
.flatMap { _ in
|
||||||
|
return self.repository.getTransactions(from: startDate, to: endDate)
|
||||||
|
}
|
||||||
|
.map { transactions in
|
||||||
|
// ✅ Business logic in UseCase
|
||||||
|
return self.filterValidTransactions(transactions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateDateRange(_ start: Date, _ end: Date) -> Single<Void> {
|
||||||
|
guard start <= end else {
|
||||||
|
return .error(TransactionError.invalidDateRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
let daysDiff = Calendar.current.dateComponents([.day], from: start, to: end).day!
|
||||||
|
guard daysDiff <= 90 else {
|
||||||
|
return .error(TransactionError.dateRangeTooLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .just(())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func filterValidTransactions(_ transactions: [Transaction]) -> [Transaction] {
|
||||||
|
return transactions.filter { $0.amount > 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRESENTATION LAYER - ViewModel
|
||||||
|
class TransactionViewModel: BaseViewModel<TransactionState> {
|
||||||
|
private let transactionUC: TransactionUseCase // ✅ Depends on UseCase
|
||||||
|
|
||||||
|
func loadTransactions(from startDate: Date, to endDate: Date) {
|
||||||
|
// ✅ Simply delegates to UseCase
|
||||||
|
transactionUC.getTransactions(from: startDate, to: endDate)
|
||||||
|
.subscribe(
|
||||||
|
onNext: { [weak self] transactions in
|
||||||
|
self?.transactions.accept(transactions)
|
||||||
|
self?.setState(.loaded)
|
||||||
|
},
|
||||||
|
onError: { [weak self] error in
|
||||||
|
self?.setState(.error(error))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- ✅ Clear separation of concerns
|
||||||
|
- ✅ Business logic testable in isolation
|
||||||
|
- ✅ ViewModel is simple coordinator
|
||||||
|
- ✅ Can reuse UseCase elsewhere
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Benefits
|
||||||
|
|
||||||
|
### With Clean Architecture
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class PaymentUseCaseTests: XCTestCase {
|
||||||
|
func testExecute_WithValidAmount_ProcessesPayment() {
|
||||||
|
// ✅ Can test UseCase in isolation
|
||||||
|
let mockRepository = MockPaymentRepository()
|
||||||
|
mockRepository.processPaymentResult = .just(PaymentResponse.success)
|
||||||
|
|
||||||
|
let useCase = PaymentUseCaseImpl(paymentRepository: mockRepository)
|
||||||
|
|
||||||
|
// Test business logic directly
|
||||||
|
let result = try! useCase.execute(amount: "10000").toBlocking().first()!
|
||||||
|
XCTAssertTrue(result.isSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExecute_WithInvalidAmount_ReturnsError() {
|
||||||
|
let mockRepository = MockPaymentRepository()
|
||||||
|
let useCase = PaymentUseCaseImpl(paymentRepository: mockRepository)
|
||||||
|
|
||||||
|
// Test validation logic
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try useCase.execute(amount: "500").toBlocking().first()
|
||||||
|
) { error in
|
||||||
|
XCTAssertEqual(error as? PaymentError, .invalidAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Without Clean Architecture
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ Hard to test business logic without UI/Network
|
||||||
|
class PaymentViewModelTests: XCTestCase {
|
||||||
|
func testProcessPayment() {
|
||||||
|
// Need to mock UI, network, and test everything together
|
||||||
|
// Business logic mixed with presentation logic
|
||||||
|
// Hard to isolate what's being tested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist for Reviews
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Clean Architecture Checklist
|
||||||
|
|
||||||
|
### Layer Separation
|
||||||
|
- [ ] ViewModel only depends on UseCase (not API/Repository)
|
||||||
|
- [ ] UseCase contains all business logic
|
||||||
|
- [ ] UseCase only depends on Repository protocol
|
||||||
|
- [ ] Repository implementation is in Data layer
|
||||||
|
- [ ] No business logic in ViewModel
|
||||||
|
- [ ] No business logic in Repository
|
||||||
|
- [ ] No UI code in Domain/Data layers
|
||||||
|
|
||||||
|
### Patterns
|
||||||
|
- [ ] ViewModel extends BaseViewModel<State>
|
||||||
|
- [ ] UseCase follows protocol + implementation pattern
|
||||||
|
- [ ] Repository follows protocol + implementation pattern
|
||||||
|
- [ ] State management uses setState()
|
||||||
|
|
||||||
|
### Dependency Injection
|
||||||
|
- [ ] All dependencies injected via constructor
|
||||||
|
- [ ] No direct instantiation of dependencies
|
||||||
|
- [ ] Swinject container properly configured
|
||||||
|
- [ ] Dependencies are protocol-based (not concrete types)
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
- [ ] UI → ViewController → ViewModel → UseCase → Repository → API/DB
|
||||||
|
- [ ] Never skips layers
|
||||||
|
- [ ] Observables/Singles for async operations
|
||||||
|
- [ ] Errors properly propagated through layers
|
||||||
|
```
|
||||||
163
skills/copilot-agent-builder/SKILL.md
Normal file
163
skills/copilot-agent-builder/SKILL.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
name: copilot-agent-builder
|
||||||
|
description: Generate custom GitHub Copilot agents (.agent.md files) for VS Code with proper YAML frontmatter, tools configuration, and handoff workflows. Use when "create copilot agent", "generate github copilot agent", "new copilot agent for", "make a copilot agent", or "build copilot agent".
|
||||||
|
allowed-tools:
|
||||||
|
- Read
|
||||||
|
- Write
|
||||||
|
- Glob
|
||||||
|
- Bash
|
||||||
|
- AskUserQuestion
|
||||||
|
---
|
||||||
|
|
||||||
|
# GitHub Copilot Agent Builder
|
||||||
|
|
||||||
|
Generate custom GitHub Copilot agents following VS Code's `.agent.md` format with proper YAML frontmatter and markdown instructions.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
Trigger this skill when user says:
|
||||||
|
- "create copilot agent"
|
||||||
|
- "generate github copilot agent"
|
||||||
|
- "new copilot agent for [purpose]"
|
||||||
|
- "make a copilot agent"
|
||||||
|
- "build copilot agent"
|
||||||
|
- "copilot custom agent"
|
||||||
|
|
||||||
|
## Agent Creation Process
|
||||||
|
|
||||||
|
### Step 1: Gather Requirements
|
||||||
|
|
||||||
|
Ask user for agent configuration:
|
||||||
|
|
||||||
|
1. **Agent Purpose & Name**:
|
||||||
|
- What should the agent do? (e.g., "plan features", "review security", "write tests")
|
||||||
|
- Suggested name: Extract from purpose (e.g., "planner", "security-reviewer", "test-writer")
|
||||||
|
|
||||||
|
2. **Tools Selection** (multi-select):
|
||||||
|
- `fetch` - Retrieve web content and documentation
|
||||||
|
- `search` - Search codebase and files
|
||||||
|
- `githubRepo` - Access GitHub repository data
|
||||||
|
- `usages` - Find code references and usage patterns
|
||||||
|
- `files` - File operations
|
||||||
|
- Custom tools if available
|
||||||
|
|
||||||
|
3. **Handoff Workflows** (optional):
|
||||||
|
- Should this agent hand off to another? (e.g., planner → coder)
|
||||||
|
- Handoff agent name
|
||||||
|
- Handoff prompt text
|
||||||
|
- Auto-send handoff? (yes/no)
|
||||||
|
|
||||||
|
4. **Additional Configuration** (optional):
|
||||||
|
- Specific model to use?
|
||||||
|
- Argument hint for users?
|
||||||
|
|
||||||
|
### Step 2: Validate Directory Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure .github/agents directory exists
|
||||||
|
mkdir -p .github/agents
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Generate Agent File
|
||||||
|
|
||||||
|
Create `.github/agents/{agent-name}.agent.md` with:
|
||||||
|
|
||||||
|
**YAML Frontmatter Structure**:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
description: [Brief overview shown in chat input]
|
||||||
|
name: [Agent identifier, lowercase with hyphens]
|
||||||
|
tools: [Array of tool names]
|
||||||
|
handoffs: # Optional
|
||||||
|
- label: [Button text]
|
||||||
|
agent: [Target agent name]
|
||||||
|
prompt: [Handoff message]
|
||||||
|
send: [true/false - auto-submit?]
|
||||||
|
model: [Optional - specific model name]
|
||||||
|
argument-hint: [Optional - user guidance text]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**Markdown Body**:
|
||||||
|
- Clear role definition
|
||||||
|
- Specific instructions
|
||||||
|
- Tool usage guidance (reference as `#tool:toolname`)
|
||||||
|
- Output format expectations
|
||||||
|
- Constraints and guidelines
|
||||||
|
|
||||||
|
### Step 4: Validate Format
|
||||||
|
|
||||||
|
Ensure generated file has:
|
||||||
|
- ✓ Valid YAML frontmatter with `---` delimiters
|
||||||
|
- ✓ All required fields (description, name)
|
||||||
|
- ✓ Tools array properly formatted
|
||||||
|
- ✓ Handoffs array if specified (with label, agent, prompt, send)
|
||||||
|
- ✓ Markdown instructions that are clear and actionable
|
||||||
|
|
||||||
|
### Step 5: Confirm Creation
|
||||||
|
|
||||||
|
Show user:
|
||||||
|
```markdown
|
||||||
|
✅ GitHub Copilot Agent Created
|
||||||
|
|
||||||
|
📁 Location: `.github/agents/{name}.agent.md`
|
||||||
|
🎯 Agent: {name}
|
||||||
|
📝 Description: {description}
|
||||||
|
🛠️ Tools: {tools list}
|
||||||
|
|
||||||
|
To use:
|
||||||
|
1. Reload VS Code window (Cmd/Ctrl + Shift + P → "Reload Window")
|
||||||
|
2. Open GitHub Copilot Chat
|
||||||
|
3. Type `@{name}` to invoke your custom agent
|
||||||
|
|
||||||
|
{If handoffs configured: "This agent can hand off to: {handoff targets}"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
After creation, provide:
|
||||||
|
|
||||||
|
1. **File path confirmation**
|
||||||
|
2. **Configuration summary** (name, description, tools, handoffs)
|
||||||
|
3. **Usage instructions** (how to reload and use)
|
||||||
|
4. **Next steps** (testing suggestions)
|
||||||
|
|
||||||
|
## Reference Documentation
|
||||||
|
|
||||||
|
- Templates and structures → `templates.md`
|
||||||
|
- Real-world examples → `examples.md`
|
||||||
|
|
||||||
|
## Important Guidelines
|
||||||
|
|
||||||
|
1. **Naming Convention**: Use lowercase with hyphens (e.g., `feature-planner`, `security-reviewer`)
|
||||||
|
2. **Description**: Brief (1-2 sentences), shown in chat input UI
|
||||||
|
3. **Tools**: Only include tools the agent actually needs
|
||||||
|
4. **Handoffs**: Enable multi-step workflows (planning → implementation → testing)
|
||||||
|
5. **Instructions**: Be specific about what the agent should and shouldn't do
|
||||||
|
6. **Tool References**: Use `#tool:toolname` syntax in markdown body
|
||||||
|
|
||||||
|
## Common Agent Patterns
|
||||||
|
|
||||||
|
**Planning Agent**:
|
||||||
|
- Tools: `fetch`, `search`, `githubRepo`, `usages`
|
||||||
|
- Handoff: To implementation agent
|
||||||
|
- Focus: Analysis and planning, no code edits
|
||||||
|
|
||||||
|
**Implementation Agent**:
|
||||||
|
- Tools: `search`, `files`, `usages`
|
||||||
|
- Handoff: To testing agent
|
||||||
|
- Focus: Write and modify code
|
||||||
|
|
||||||
|
**Review Agent**:
|
||||||
|
- Tools: `search`, `githubRepo`, `usages`
|
||||||
|
- Handoff: Back to implementation for fixes
|
||||||
|
- Focus: Quality, security, performance checks
|
||||||
|
|
||||||
|
**Testing Agent**:
|
||||||
|
- Tools: `search`, `files`
|
||||||
|
- Handoff: Back to implementation or to review
|
||||||
|
- Focus: Test creation and validation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to build custom GitHub Copilot agents!**
|
||||||
651
skills/copilot-agent-builder/examples.md
Normal file
651
skills/copilot-agent-builder/examples.md
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
# GitHub Copilot Agent Examples
|
||||||
|
|
||||||
|
## Example 1: Feature Planner Agent
|
||||||
|
|
||||||
|
**Use Case**: Planning new features without implementation
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Analyze requirements and create detailed implementation plans
|
||||||
|
name: feature-planner
|
||||||
|
tools: ['fetch', 'search', 'githubRepo', 'usages']
|
||||||
|
handoffs:
|
||||||
|
- label: Start Implementation
|
||||||
|
agent: agent
|
||||||
|
prompt: Implement the feature plan outlined above step by step.
|
||||||
|
send: false
|
||||||
|
model: gpt-4
|
||||||
|
argument-hint: Describe the feature you want to plan
|
||||||
|
---
|
||||||
|
|
||||||
|
# Feature Planning Mode
|
||||||
|
|
||||||
|
You are in planning mode. Your goal is to analyze requirements and create comprehensive implementation plans WITHOUT making any code changes.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. **Understand Requirements**: Ask clarifying questions about the feature
|
||||||
|
2. **Research Context**: Use #tool:search to understand existing codebase patterns
|
||||||
|
3. **Design Solution**: Create architecture and implementation approach
|
||||||
|
4. **Plan Steps**: Break down into actionable implementation steps
|
||||||
|
5. **Identify Risks**: Note potential challenges and dependencies
|
||||||
|
|
||||||
|
## Research Tools
|
||||||
|
|
||||||
|
- Use #tool:fetch to retrieve documentation for libraries or best practices
|
||||||
|
- Use #tool:githubRepo to analyze repository structure and conventions
|
||||||
|
- Use #tool:usages to find how similar features are implemented
|
||||||
|
- Use #tool:search to locate relevant existing code
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
Create a plan with these sections:
|
||||||
|
|
||||||
|
**Requirements Analysis**
|
||||||
|
- Feature overview
|
||||||
|
- User stories
|
||||||
|
- Acceptance criteria
|
||||||
|
|
||||||
|
**Technical Design**
|
||||||
|
- Architecture decisions
|
||||||
|
- Component interactions
|
||||||
|
- Data flow
|
||||||
|
|
||||||
|
**Implementation Plan**
|
||||||
|
1. File structure changes needed
|
||||||
|
2. Step-by-step implementation sequence
|
||||||
|
3. Configuration updates
|
||||||
|
4. Database migrations (if applicable)
|
||||||
|
|
||||||
|
**Testing Strategy**
|
||||||
|
- Unit test requirements
|
||||||
|
- Integration test scenarios
|
||||||
|
- Manual testing checklist
|
||||||
|
|
||||||
|
**Risks & Considerations**
|
||||||
|
- Technical challenges
|
||||||
|
- Dependencies on other systems
|
||||||
|
- Performance implications
|
||||||
|
- Security considerations
|
||||||
|
|
||||||
|
## Important Guidelines
|
||||||
|
|
||||||
|
- **NO CODE CHANGES**: You only create plans, never implement
|
||||||
|
- **BE SPECIFIC**: Include file paths, function names, specific changes
|
||||||
|
- **CONSIDER CONTEXT**: Align with existing patterns and conventions
|
||||||
|
- **IDENTIFY GAPS**: Note missing information or unclear requirements
|
||||||
|
- **ENABLE HANDOFF**: Make plans detailed enough for another agent to implement
|
||||||
|
|
||||||
|
After completing the plan, offer the "Start Implementation" handoff to transition to the coding agent.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 2: Security Reviewer Agent
|
||||||
|
|
||||||
|
**Use Case**: Security-focused code review
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Review code for security vulnerabilities and best practices
|
||||||
|
name: security-reviewer
|
||||||
|
tools: ['search', 'githubRepo', 'usages']
|
||||||
|
handoffs:
|
||||||
|
- label: Fix Security Issues
|
||||||
|
agent: agent
|
||||||
|
prompt: Address the security vulnerabilities identified in the review.
|
||||||
|
send: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security Code Review Agent
|
||||||
|
|
||||||
|
You are a security-focused code reviewer specializing in identifying vulnerabilities and enforcing security best practices.
|
||||||
|
|
||||||
|
## Security Review Checklist
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
- [ ] All user inputs are validated and sanitized
|
||||||
|
- [ ] No SQL injection vulnerabilities
|
||||||
|
- [ ] No command injection vulnerabilities
|
||||||
|
- [ ] File paths are validated against directory traversal
|
||||||
|
|
||||||
|
### Authentication & Authorization
|
||||||
|
- [ ] Authentication is properly implemented
|
||||||
|
- [ ] Authorization checks are in place
|
||||||
|
- [ ] Session management is secure
|
||||||
|
- [ ] Passwords are hashed (never plain text)
|
||||||
|
- [ ] Multi-factor authentication considered
|
||||||
|
|
||||||
|
### Data Protection
|
||||||
|
- [ ] Sensitive data is encrypted at rest
|
||||||
|
- [ ] TLS/HTTPS used for data in transit
|
||||||
|
- [ ] No secrets in code or version control
|
||||||
|
- [ ] PII is properly protected
|
||||||
|
|
||||||
|
### Common Vulnerabilities (OWASP Top 10)
|
||||||
|
- [ ] No XSS vulnerabilities
|
||||||
|
- [ ] No CSRF vulnerabilities
|
||||||
|
- [ ] No insecure deserialization
|
||||||
|
- [ ] No XML external entity (XXE) attacks
|
||||||
|
- [ ] Proper security headers configured
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- [ ] Dependencies are up to date
|
||||||
|
- [ ] No known vulnerable dependencies
|
||||||
|
- [ ] License compliance checked
|
||||||
|
|
||||||
|
## Review Process
|
||||||
|
|
||||||
|
1. Use #tool:search to scan for common vulnerability patterns
|
||||||
|
2. Use #tool:githubRepo to check security configuration files
|
||||||
|
3. Use #tool:usages to verify security functions are used correctly
|
||||||
|
4. Cross-reference against OWASP Top 10 and CWE database
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Security Review Report
|
||||||
|
|
||||||
|
🔒 **Security Status**: [PASS / NEEDS ATTENTION / CRITICAL]
|
||||||
|
|
||||||
|
### 🔴 Critical Vulnerabilities (Fix Immediately)
|
||||||
|
- [ ] **File**: `path/to/file.ts:42`
|
||||||
|
- **Issue**: SQL Injection vulnerability
|
||||||
|
- **Details**: User input directly concatenated into SQL query
|
||||||
|
- **Fix**: Use parameterized queries
|
||||||
|
- **Example**:
|
||||||
|
```typescript
|
||||||
|
// ❌ Vulnerable
|
||||||
|
const query = `SELECT * FROM users WHERE id = ${userId}`;
|
||||||
|
|
||||||
|
// ✅ Secure
|
||||||
|
const query = 'SELECT * FROM users WHERE id = ?';
|
||||||
|
db.execute(query, [userId]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🟡 Medium Priority Issues
|
||||||
|
- [ ] **File**: `path/to/file.ts:67`
|
||||||
|
- **Issue**: [Description]
|
||||||
|
- **Fix**: [Recommendation]
|
||||||
|
|
||||||
|
### ✅ Security Strengths
|
||||||
|
- Proper input validation in authentication flow
|
||||||
|
- TLS configured correctly
|
||||||
|
- Security headers implemented
|
||||||
|
|
||||||
|
### 📊 Summary
|
||||||
|
- **Critical**: 1
|
||||||
|
- **High**: 0
|
||||||
|
- **Medium**: 2
|
||||||
|
- **Low**: 3
|
||||||
|
|
||||||
|
### Recommended Actions
|
||||||
|
1. [Priority 1 action]
|
||||||
|
2. [Priority 2 action]
|
||||||
|
```
|
||||||
|
|
||||||
|
Use "Fix Security Issues" handoff to address vulnerabilities.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 3: API Documentation Generator
|
||||||
|
|
||||||
|
**Use Case**: Automatic API documentation
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Generate comprehensive API documentation from code
|
||||||
|
name: api-documenter
|
||||||
|
tools: ['search', 'files', 'githubRepo']
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Documentation Generator
|
||||||
|
|
||||||
|
You are a technical writer specializing in API documentation. Generate clear, comprehensive documentation for APIs.
|
||||||
|
|
||||||
|
## Documentation Process
|
||||||
|
|
||||||
|
1. **Discover APIs**: Use #tool:search to find API endpoints and functions
|
||||||
|
2. **Analyze Structure**: Use #tool:githubRepo to understand project organization
|
||||||
|
3. **Extract Details**: Parse parameters, return types, and examples
|
||||||
|
4. **Generate Docs**: Create structured documentation files
|
||||||
|
|
||||||
|
## Documentation Format
|
||||||
|
|
||||||
|
For each API endpoint or function, document:
|
||||||
|
|
||||||
|
### REST API Endpoint
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### [HTTP METHOD] `/api/endpoint/path`
|
||||||
|
|
||||||
|
**Description**: [What this endpoint does]
|
||||||
|
|
||||||
|
**Authentication**: [Required/Optional - Type]
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
| Name | Type | Required | Description |
|
||||||
|
|------|------|----------|-------------|
|
||||||
|
| `param1` | string | Yes | [Description] |
|
||||||
|
| `param2` | number | No | [Description] |
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"field1": "value",
|
||||||
|
"field2": 123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "123",
|
||||||
|
"name": "Example"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses**:
|
||||||
|
- `400 Bad Request`: Invalid parameters
|
||||||
|
- `401 Unauthorized`: Missing or invalid authentication
|
||||||
|
- `404 Not Found`: Resource not found
|
||||||
|
- `500 Internal Server Error`: Server error
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```bash
|
||||||
|
curl -X POST https://api.example.com/api/endpoint/path \
|
||||||
|
-H "Authorization: Bearer TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"field1": "value", "field2": 123}'
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function/Method
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### `functionName(param1, param2)`
|
||||||
|
|
||||||
|
**Description**: [What this function does]
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
- `param1` (Type): [Description]
|
||||||
|
- `param2` (Type): [Description]
|
||||||
|
|
||||||
|
**Returns**: [Return type and description]
|
||||||
|
|
||||||
|
**Throws**: [Exceptions if any]
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```typescript
|
||||||
|
const result = functionName('value', 42);
|
||||||
|
console.log(result); // Expected output
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
Create documentation files following this hierarchy:
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── api/
|
||||||
|
│ ├── README.md (Overview)
|
||||||
|
│ ├── authentication.md
|
||||||
|
│ ├── endpoints/
|
||||||
|
│ │ ├── users.md
|
||||||
|
│ │ ├── products.md
|
||||||
|
│ │ └── orders.md
|
||||||
|
│ └── errors.md
|
||||||
|
└── guides/
|
||||||
|
├── getting-started.md
|
||||||
|
└── examples.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Include working code examples for all endpoints
|
||||||
|
- Document error cases and status codes
|
||||||
|
- Provide authentication examples
|
||||||
|
- Show request/response examples with real data structures
|
||||||
|
- Include rate limiting information
|
||||||
|
- Link related endpoints
|
||||||
|
- Keep examples up to date with code changes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 4: Test Generator Agent
|
||||||
|
|
||||||
|
**Use Case**: Automatic test creation
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Generate comprehensive test suites with high coverage
|
||||||
|
name: test-generator
|
||||||
|
tools: ['search', 'files', 'usages']
|
||||||
|
handoffs:
|
||||||
|
- label: Run Tests
|
||||||
|
agent: agent
|
||||||
|
prompt: Execute the generated tests and report results.
|
||||||
|
send: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Comprehensive Test Generator
|
||||||
|
|
||||||
|
You are a testing specialist who creates thorough, maintainable test suites.
|
||||||
|
|
||||||
|
## Test Generation Process
|
||||||
|
|
||||||
|
1. **Analyze Code**: Use #tool:search to understand what needs testing
|
||||||
|
2. **Find Patterns**: Use #tool:usages to see how code is used in practice
|
||||||
|
3. **Create Tests**: Use #tool:files to generate test files
|
||||||
|
4. **Ensure Coverage**: Cover happy paths, edge cases, and error scenarios
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
### Unit Test Template
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from '@testing-framework';
|
||||||
|
import { FunctionToTest } from '../src/module';
|
||||||
|
|
||||||
|
describe('FunctionToTest', () => {
|
||||||
|
let testContext: TestContext;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Setup
|
||||||
|
testContext = createTestContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Cleanup
|
||||||
|
testContext.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Happy Path', () => {
|
||||||
|
it('should handle valid input correctly', () => {
|
||||||
|
const result = FunctionToTest('valid-input');
|
||||||
|
expect(result).toBe('expected-output');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process multiple items', () => {
|
||||||
|
const results = FunctionToTest(['item1', 'item2']);
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0]).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('should handle empty input', () => {
|
||||||
|
const result = FunctionToTest('');
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null/undefined', () => {
|
||||||
|
expect(() => FunctionToTest(null)).toThrow();
|
||||||
|
expect(() => FunctionToTest(undefined)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle maximum values', () => {
|
||||||
|
const largeInput = 'x'.repeat(10000);
|
||||||
|
const result = FunctionToTest(largeInput);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handling', () => {
|
||||||
|
it('should throw on invalid input', () => {
|
||||||
|
expect(() => FunctionToTest('invalid')).toThrow('Invalid input');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle async errors gracefully', async () => {
|
||||||
|
await expect(
|
||||||
|
FunctionToTest('trigger-error')
|
||||||
|
).rejects.toThrow('Expected error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Coverage Goals
|
||||||
|
|
||||||
|
Create tests for:
|
||||||
|
- ✅ **Happy Path** (70%): Normal, expected usage
|
||||||
|
- ✅ **Edge Cases** (20%): Boundary conditions, empty values, max values
|
||||||
|
- ✅ **Error Cases** (10%): Invalid input, exceptions, failures
|
||||||
|
|
||||||
|
## Test Categories
|
||||||
|
|
||||||
|
### 1. Unit Tests
|
||||||
|
- Test individual functions in isolation
|
||||||
|
- Mock dependencies
|
||||||
|
- Fast execution
|
||||||
|
|
||||||
|
### 2. Integration Tests
|
||||||
|
- Test component interactions
|
||||||
|
- Use real dependencies where appropriate
|
||||||
|
- Verify data flow
|
||||||
|
|
||||||
|
### 3. API Tests
|
||||||
|
- Test endpoints with various payloads
|
||||||
|
- Verify status codes and responses
|
||||||
|
- Test authentication and authorization
|
||||||
|
|
||||||
|
### 4. UI Tests (if applicable)
|
||||||
|
- Test user interactions
|
||||||
|
- Verify rendering
|
||||||
|
- Test accessibility
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Generated Test Suite
|
||||||
|
|
||||||
|
### 📁 Test Files Created
|
||||||
|
- `tests/unit/module.test.ts` (15 test cases)
|
||||||
|
- `tests/integration/api.test.ts` (8 test cases)
|
||||||
|
- `tests/e2e/user-flow.test.ts` (5 test cases)
|
||||||
|
|
||||||
|
### 📊 Coverage Summary
|
||||||
|
- **Total Test Cases**: 28
|
||||||
|
- **Happy Path**: 18 tests (64%)
|
||||||
|
- **Edge Cases**: 7 tests (25%)
|
||||||
|
- **Error Scenarios**: 3 tests (11%)
|
||||||
|
|
||||||
|
### 🎯 Coverage Areas
|
||||||
|
✅ User authentication flow
|
||||||
|
✅ Data validation
|
||||||
|
✅ API error handling
|
||||||
|
✅ Edge cases (null, empty, max values)
|
||||||
|
⚠️ Performance testing (manual review needed)
|
||||||
|
|
||||||
|
### ▶️ Run Tests
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run specific suite
|
||||||
|
npm test -- tests/unit/module.test.ts
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
npm test -- --coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📝 Manual Testing Checklist
|
||||||
|
- [ ] Performance with large datasets
|
||||||
|
- [ ] UI responsiveness on mobile
|
||||||
|
- [ ] Cross-browser compatibility
|
||||||
|
```
|
||||||
|
|
||||||
|
Use "Run Tests" handoff to execute and review results.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 5: Migration Planner Agent
|
||||||
|
|
||||||
|
**Use Case**: Planning code migrations and refactoring
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Plan and guide large-scale code migrations and refactoring
|
||||||
|
name: migration-planner
|
||||||
|
tools: ['search', 'githubRepo', 'usages', 'fetch']
|
||||||
|
handoffs:
|
||||||
|
- label: Execute Migration
|
||||||
|
agent: agent
|
||||||
|
prompt: Execute the migration plan step by step with verification at each stage.
|
||||||
|
send: false
|
||||||
|
argument-hint: Describe the migration (e.g., "migrate from Vue 2 to Vue 3")
|
||||||
|
---
|
||||||
|
|
||||||
|
# Migration Planning Agent
|
||||||
|
|
||||||
|
You are a migration specialist who creates detailed, safe migration plans for large-scale code changes.
|
||||||
|
|
||||||
|
## Migration Planning Process
|
||||||
|
|
||||||
|
### 1. Assess Current State
|
||||||
|
- Use #tool:search to inventory existing code patterns
|
||||||
|
- Use #tool:githubRepo to understand project structure
|
||||||
|
- Use #tool:usages to find all usage locations
|
||||||
|
- Use #tool:fetch to research migration guides and best practices
|
||||||
|
|
||||||
|
### 2. Identify Impact
|
||||||
|
- List all files affected
|
||||||
|
- Identify breaking changes
|
||||||
|
- Map dependencies
|
||||||
|
- Estimate effort
|
||||||
|
|
||||||
|
### 3. Create Migration Plan
|
||||||
|
- Break into phases
|
||||||
|
- Define rollback strategy
|
||||||
|
- Plan testing approach
|
||||||
|
- Schedule timeline
|
||||||
|
|
||||||
|
### 4. Document Steps
|
||||||
|
- Detailed step-by-step instructions
|
||||||
|
- Verification steps
|
||||||
|
- Rollback procedures
|
||||||
|
|
||||||
|
## Migration Plan Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Migration Plan: [From X to Y]
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
- **Scope**: [What's being migrated]
|
||||||
|
- **Impact**: [How many files/components affected]
|
||||||
|
- **Estimated Effort**: [Time estimate]
|
||||||
|
- **Risk Level**: [Low/Medium/High]
|
||||||
|
|
||||||
|
## Current State Analysis
|
||||||
|
|
||||||
|
### Inventory
|
||||||
|
- Total files using old pattern: [N]
|
||||||
|
- Key dependencies: [List]
|
||||||
|
- Breaking changes: [List]
|
||||||
|
|
||||||
|
### Usage Patterns Found
|
||||||
|
```typescript
|
||||||
|
// Pattern 1: [Found in X files]
|
||||||
|
// Pattern 2: [Found in Y files]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
### Phase 1: Preparation
|
||||||
|
1. [ ] Update dependencies
|
||||||
|
2. [ ] Create feature flags
|
||||||
|
3. [ ] Set up parallel testing
|
||||||
|
4. [ ] Document current behavior
|
||||||
|
|
||||||
|
### Phase 2: Incremental Migration
|
||||||
|
1. [ ] Migrate utility functions (5 files)
|
||||||
|
2. [ ] Migrate components (12 files)
|
||||||
|
3. [ ] Update tests (8 files)
|
||||||
|
4. [ ] Update documentation
|
||||||
|
|
||||||
|
### Phase 3: Validation
|
||||||
|
1. [ ] Run full test suite
|
||||||
|
2. [ ] Perform manual testing
|
||||||
|
3. [ ] Load testing
|
||||||
|
4. [ ] Security review
|
||||||
|
|
||||||
|
### Phase 4: Cleanup
|
||||||
|
1. [ ] Remove old code
|
||||||
|
2. [ ] Remove feature flags
|
||||||
|
3. [ ] Update dependencies
|
||||||
|
4. [ ] Archive documentation
|
||||||
|
|
||||||
|
## Detailed Steps
|
||||||
|
|
||||||
|
### Step 1: [Step Name]
|
||||||
|
**Files to modify**: `file1.ts`, `file2.ts`
|
||||||
|
**Changes**:
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
oldPattern();
|
||||||
|
|
||||||
|
// After
|
||||||
|
newPattern();
|
||||||
|
```
|
||||||
|
**Verification**:
|
||||||
|
```bash
|
||||||
|
npm test -- file1.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If issues occur at any phase:
|
||||||
|
1. Revert to commit: `[hash]`
|
||||||
|
2. Disable feature flag: `MIGRATION_ENABLED=false`
|
||||||
|
3. Restore from backup: `backup-[date]`
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Automated Tests
|
||||||
|
- Unit tests for each migrated file
|
||||||
|
- Integration tests for workflows
|
||||||
|
- Regression tests for unchanged behavior
|
||||||
|
|
||||||
|
### Manual Tests
|
||||||
|
- [ ] User flow 1
|
||||||
|
- [ ] User flow 2
|
||||||
|
- [ ] Edge case scenarios
|
||||||
|
|
||||||
|
## Risk Mitigation
|
||||||
|
|
||||||
|
| Risk | Impact | Probability | Mitigation |
|
||||||
|
|------|--------|-------------|------------|
|
||||||
|
| Breaking change in production | High | Low | Feature flags, gradual rollout |
|
||||||
|
| Performance degradation | Medium | Medium | Load testing before deployment |
|
||||||
|
| Data loss | High | Low | Database backups, dry runs |
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
- **Week 1**: Preparation and dependency updates
|
||||||
|
- **Week 2**: Phase 1 migration (utilities)
|
||||||
|
- **Week 3**: Phase 2 migration (components)
|
||||||
|
- **Week 4**: Testing and validation
|
||||||
|
- **Week 5**: Production deployment and monitoring
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] All tests passing
|
||||||
|
- [ ] No performance degradation
|
||||||
|
- [ ] Zero production incidents
|
||||||
|
- [ ] Documentation updated
|
||||||
|
- [ ] Team trained on new patterns
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Deliverables
|
||||||
|
|
||||||
|
1. **Migration Plan Document** (as shown above)
|
||||||
|
2. **File-by-File Checklist** (detailed change list)
|
||||||
|
3. **Testing Scripts** (validation automation)
|
||||||
|
4. **Rollback Procedures** (emergency recovery)
|
||||||
|
5. **Team Communication** (stakeholder updates)
|
||||||
|
|
||||||
|
Use "Execute Migration" handoff when ready to begin implementation.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Examples ready!** These demonstrate various agent types and configurations for different use cases.
|
||||||
391
skills/copilot-agent-builder/templates.md
Normal file
391
skills/copilot-agent-builder/templates.md
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
# GitHub Copilot Agent Templates
|
||||||
|
|
||||||
|
## Basic Agent Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: [One sentence describing what this agent does]
|
||||||
|
name: [agent-name]
|
||||||
|
tools: ['tool1', 'tool2', 'tool3']
|
||||||
|
---
|
||||||
|
|
||||||
|
# [Agent Name] Instructions
|
||||||
|
|
||||||
|
You are a [role description]. Your primary responsibility is to [main purpose].
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
|
||||||
|
1. [Responsibility 1]
|
||||||
|
2. [Responsibility 2]
|
||||||
|
3. [Responsibility 3]
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- [Guideline 1]
|
||||||
|
- [Guideline 2]
|
||||||
|
- [Guideline 3]
|
||||||
|
|
||||||
|
## Tool Usage
|
||||||
|
|
||||||
|
- Use #tool:search to [purpose]
|
||||||
|
- Use #tool:fetch to [purpose]
|
||||||
|
- Use #tool:githubRepo to [purpose]
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
[Describe expected output structure]
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- DO: [What to do]
|
||||||
|
- DON'T: [What to avoid]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Planning Agent Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Generate implementation plans for features and tasks
|
||||||
|
name: planner
|
||||||
|
tools: ['fetch', 'search', 'githubRepo', 'usages']
|
||||||
|
handoffs:
|
||||||
|
- label: Implement Plan
|
||||||
|
agent: coder
|
||||||
|
prompt: Implement the plan outlined above.
|
||||||
|
send: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Feature Planning Agent
|
||||||
|
|
||||||
|
You are a technical planner who creates detailed implementation plans without making code changes.
|
||||||
|
|
||||||
|
## Your Role
|
||||||
|
|
||||||
|
Analyze requirements and create comprehensive implementation plans that include:
|
||||||
|
- Architecture decisions
|
||||||
|
- File changes needed
|
||||||
|
- Step-by-step implementation approach
|
||||||
|
- Potential risks and considerations
|
||||||
|
- Testing strategy
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- **NO CODE EDITS**: Only plan, never implement
|
||||||
|
- Use #tool:search to understand existing codebase patterns
|
||||||
|
- Use #tool:fetch to retrieve documentation and best practices
|
||||||
|
- Use #tool:githubRepo to analyze repository structure
|
||||||
|
- Use #tool:usages to find how similar features are implemented
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
[Brief summary]
|
||||||
|
|
||||||
|
### Architecture Changes
|
||||||
|
- [Change 1]
|
||||||
|
- [Change 2]
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
1. `path/to/file1.ts` - [What to change]
|
||||||
|
2. `path/to/file2.ts` - [What to change]
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. [Step 1]
|
||||||
|
2. [Step 2]
|
||||||
|
3. [Step 3]
|
||||||
|
|
||||||
|
### Testing Approach
|
||||||
|
- [Test requirement 1]
|
||||||
|
- [Test requirement 2]
|
||||||
|
|
||||||
|
### Risks & Considerations
|
||||||
|
- [Risk 1]
|
||||||
|
- [Risk 2]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
When ready, use the "Implement Plan" button to transition to the coder agent.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Agent Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Implement code changes following the provided plan
|
||||||
|
name: coder
|
||||||
|
tools: ['search', 'files', 'usages']
|
||||||
|
handoffs:
|
||||||
|
- label: Review Code
|
||||||
|
agent: reviewer
|
||||||
|
prompt: Review the implementation for quality and best practices.
|
||||||
|
send: false
|
||||||
|
- label: Write Tests
|
||||||
|
agent: tester
|
||||||
|
prompt: Create comprehensive tests for the implemented changes.
|
||||||
|
send: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Implementation Agent
|
||||||
|
|
||||||
|
You are a software engineer who implements code changes following best practices and the provided plan.
|
||||||
|
|
||||||
|
## Your Role
|
||||||
|
|
||||||
|
Write high-quality, maintainable code that:
|
||||||
|
- Follows the implementation plan
|
||||||
|
- Adheres to project conventions
|
||||||
|
- Includes proper error handling
|
||||||
|
- Is well-documented with comments
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Use #tool:search to understand existing code patterns
|
||||||
|
- Use #tool:files to create and modify files
|
||||||
|
- Use #tool:usages to ensure consistency with existing usage patterns
|
||||||
|
- Follow the project's coding standards and conventions
|
||||||
|
- Keep changes focused and atomic
|
||||||
|
|
||||||
|
## Implementation Process
|
||||||
|
|
||||||
|
1. **Understand Context**: Review the plan and existing code
|
||||||
|
2. **Implement Changes**: Make the necessary code modifications
|
||||||
|
3. **Add Documentation**: Include comments and docstrings
|
||||||
|
4. **Verify Consistency**: Ensure changes align with codebase patterns
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
For each file modified:
|
||||||
|
```
|
||||||
|
✅ Modified: `path/to/file.ts`
|
||||||
|
- [Change description 1]
|
||||||
|
- [Change description 2]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handoffs
|
||||||
|
|
||||||
|
After implementation:
|
||||||
|
- **Review Code**: Have code reviewed for quality
|
||||||
|
- **Write Tests**: Create tests for the implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Agent Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Review code for quality, security, and best practices
|
||||||
|
name: reviewer
|
||||||
|
tools: ['search', 'githubRepo', 'usages']
|
||||||
|
handoffs:
|
||||||
|
- label: Fix Issues
|
||||||
|
agent: coder
|
||||||
|
prompt: Address the issues identified in the review.
|
||||||
|
send: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code Review Agent
|
||||||
|
|
||||||
|
You are a senior code reviewer who ensures code quality, security, and adherence to best practices.
|
||||||
|
|
||||||
|
## Your Role
|
||||||
|
|
||||||
|
Perform comprehensive code reviews checking for:
|
||||||
|
- **Security**: Vulnerabilities and security best practices
|
||||||
|
- **Quality**: Code maintainability and readability
|
||||||
|
- **Performance**: Potential performance issues
|
||||||
|
- **Best Practices**: Adherence to patterns and conventions
|
||||||
|
- **Testing**: Test coverage and quality
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Use #tool:search to compare against project conventions
|
||||||
|
- Use #tool:githubRepo to understand repository standards
|
||||||
|
- Use #tool:usages to verify consistent usage patterns
|
||||||
|
- Be constructive and specific in feedback
|
||||||
|
- Prioritize issues by severity (Critical, High, Medium, Low)
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
- [ ] No security vulnerabilities (XSS, SQL injection, etc.)
|
||||||
|
- [ ] Proper error handling
|
||||||
|
- [ ] Code follows project conventions
|
||||||
|
- [ ] No performance bottlenecks
|
||||||
|
- [ ] Adequate test coverage
|
||||||
|
- [ ] Clear documentation and comments
|
||||||
|
- [ ] No code duplication
|
||||||
|
- [ ] Consistent naming conventions
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```
|
||||||
|
## Code Review Report
|
||||||
|
|
||||||
|
### ✅ Strengths
|
||||||
|
- [Positive aspect 1]
|
||||||
|
- [Positive aspect 2]
|
||||||
|
|
||||||
|
### 🔴 Critical Issues
|
||||||
|
- [ ] `file.ts:42` - [Issue description and fix]
|
||||||
|
|
||||||
|
### 🟡 Suggestions
|
||||||
|
- [ ] `file.ts:67` - [Suggestion description]
|
||||||
|
|
||||||
|
### 📊 Summary
|
||||||
|
- Security: ✅ No issues found
|
||||||
|
- Quality: ⚠️ 2 suggestions
|
||||||
|
- Performance: ✅ Looks good
|
||||||
|
- Testing: ❌ Needs improvement
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
[Recommended actions]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
Use "Fix Issues" button to send critical issues back to implementation.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Agent Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Create comprehensive tests for code changes
|
||||||
|
name: tester
|
||||||
|
tools: ['search', 'files', 'usages']
|
||||||
|
handoffs:
|
||||||
|
- label: Review Tests
|
||||||
|
agent: reviewer
|
||||||
|
prompt: Review the test coverage and quality.
|
||||||
|
send: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testing Agent
|
||||||
|
|
||||||
|
You are a testing specialist who creates comprehensive, maintainable test suites.
|
||||||
|
|
||||||
|
## Your Role
|
||||||
|
|
||||||
|
Create high-quality tests that:
|
||||||
|
- Cover all critical paths and edge cases
|
||||||
|
- Are maintainable and readable
|
||||||
|
- Follow testing best practices
|
||||||
|
- Provide meaningful assertions
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Use #tool:search to find existing test patterns
|
||||||
|
- Use #tool:files to create test files
|
||||||
|
- Use #tool:usages to understand how code is used in practice
|
||||||
|
- Follow the project's testing framework conventions
|
||||||
|
- Aim for meaningful coverage, not just high percentages
|
||||||
|
|
||||||
|
## Test Types to Consider
|
||||||
|
|
||||||
|
1. **Unit Tests**: Individual function/method behavior
|
||||||
|
2. **Integration Tests**: Component interactions
|
||||||
|
3. **Edge Cases**: Boundary conditions and error scenarios
|
||||||
|
4. **Regression Tests**: Known bug scenarios
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```
|
||||||
|
## Test Suite Created
|
||||||
|
|
||||||
|
### 📝 Test Files
|
||||||
|
- `tests/feature.test.ts` - [Description]
|
||||||
|
- `tests/integration.test.ts` - [Description]
|
||||||
|
|
||||||
|
### ✅ Coverage
|
||||||
|
- Unit tests: [X] test cases
|
||||||
|
- Integration tests: [Y] scenarios
|
||||||
|
- Edge cases: [Z] scenarios
|
||||||
|
|
||||||
|
### 🎯 Test Summary
|
||||||
|
- Total test cases: [N]
|
||||||
|
- Coverage areas: [List key areas]
|
||||||
|
- Known gaps: [If any]
|
||||||
|
|
||||||
|
### Run Tests
|
||||||
|
```bash
|
||||||
|
npm test [test-file-pattern]
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
Use "Review Tests" to have test quality reviewed.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Agent Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Generate and update technical documentation
|
||||||
|
name: documenter
|
||||||
|
tools: ['search', 'files', 'githubRepo']
|
||||||
|
---
|
||||||
|
|
||||||
|
# Documentation Agent
|
||||||
|
|
||||||
|
You are a technical writer who creates clear, comprehensive documentation.
|
||||||
|
|
||||||
|
## Your Role
|
||||||
|
|
||||||
|
Create documentation that is:
|
||||||
|
- Clear and accessible to the target audience
|
||||||
|
- Comprehensive with examples
|
||||||
|
- Up-to-date with current implementation
|
||||||
|
- Well-structured and easy to navigate
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Use #tool:search to understand code functionality
|
||||||
|
- Use #tool:files to create/update documentation files
|
||||||
|
- Use #tool:githubRepo to understand project structure
|
||||||
|
- Include code examples and usage patterns
|
||||||
|
- Follow the project's documentation style
|
||||||
|
|
||||||
|
## Documentation Types
|
||||||
|
|
||||||
|
1. **API Documentation**: Endpoints, parameters, responses
|
||||||
|
2. **User Guides**: How to use features
|
||||||
|
3. **Developer Guides**: How to contribute/extend
|
||||||
|
4. **README**: Project overview and quick start
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# [Feature/Module Name]
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
[Brief description]
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
[Installation commands]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```[language]
|
||||||
|
[Code example]
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
### [Function/Method Name]
|
||||||
|
- **Parameters**: [List]
|
||||||
|
- **Returns**: [Type and description]
|
||||||
|
- **Example**: [Code example]
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
[Comprehensive examples]
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
[Common issues and solutions]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Templates ready for agent generation!** Use these as starting points and customize for specific needs.
|
||||||
109
skills/instruments/SKILL.md
Normal file
109
skills/instruments/SKILL.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
name: ios-instruments-performance-cli
|
||||||
|
description: Use Xcode Instruments command line tools to analyze iOS app performance, detect memory leaks, optimize launch times, monitor CPU usage, and identify performance bottlenecks for the iOS project
|
||||||
|
---
|
||||||
|
|
||||||
|
# iOS Instruments Performance CLI
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
When helping with iOS app performance optimization using Instruments command line tools:
|
||||||
|
|
||||||
|
### 1. Setup Analysis Environment
|
||||||
|
|
||||||
|
**CRITICAL: Always use device UUID, never device names**
|
||||||
|
|
||||||
|
- Get device UUID: `xcrun simctl list devices available | grep "iPhone"`
|
||||||
|
- Device names like "iPhone 17 Pro" are ambiguous and will fail
|
||||||
|
- Use UUID format: `F464E766-555C-4B95-B8CC-763702A70791`
|
||||||
|
|
||||||
|
**Clean installation state (REQUIRED)**
|
||||||
|
|
||||||
|
- Always uninstall app before profiling: `xcrun simctl uninstall $DEVICE_UUID <bundle id>`
|
||||||
|
- Multiple app installations cause "process is ambiguous" errors
|
||||||
|
- Alternative: Completely erase simulator for cleanest state
|
||||||
|
|
||||||
|
**Build configuration**
|
||||||
|
|
||||||
|
- Build in Release mode for accurate performance measurements
|
||||||
|
|
||||||
|
### 2. Choose Appropriate Instrument Template
|
||||||
|
|
||||||
|
Use `xcrun xctrace list templates` to see available templates:
|
||||||
|
|
||||||
|
- **App Launch** - Essential for launch time optimization (most common)
|
||||||
|
- **Time Profiler** - CPU performance analysis
|
||||||
|
- **Allocations** - Memory usage tracking
|
||||||
|
- **Leaks** - Memory leak detection
|
||||||
|
- **Network** - API calls and network activity
|
||||||
|
- **Animation Hitches** - UI performance issues
|
||||||
|
|
||||||
|
### 3. Run Performance Analysis
|
||||||
|
|
||||||
|
**Standard workflow:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791" # this is sample uuid, run command line to get exist uuid
|
||||||
|
xcrun simctl uninstall $DEVICE_UUID <bundle id>
|
||||||
|
xcrun simctl install $DEVICE_UUID /path/to/<app name>.app
|
||||||
|
sleep 2
|
||||||
|
xcrun xctrace record --template "App Launch" --device $DEVICE_UUID \
|
||||||
|
--launch -- /path/to/<app name>.app 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important notes:**
|
||||||
|
|
||||||
|
- Trace files auto-generate names like `Launch_<app name>.app_2025-10-30_3.55.40 PM_39E6A410.trace`
|
||||||
|
- `--output` parameter may be ignored; accept auto-generated names
|
||||||
|
- Wait 2 seconds after installation before profiling
|
||||||
|
- Use `2>&1` to capture all output
|
||||||
|
- Recording duration: 10-30s for launch, 2-5m for other analyses
|
||||||
|
|
||||||
|
### 4. Analyze Results
|
||||||
|
|
||||||
|
**Finding trace files:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -lt *.trace | head -1 # Most recent trace
|
||||||
|
find . -name "*.trace" -type d -mmin -5 # Files from last 5 minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Analysis approaches:**
|
||||||
|
|
||||||
|
- **CLI export often fails** - "trace is malformed" errors are common
|
||||||
|
- **Recommended**: Open in Instruments.app GUI for reliable analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open -a Instruments.app Launch_<app name>*.trace
|
||||||
|
```
|
||||||
|
|
||||||
|
- Parse signpost markers for performance metrics
|
||||||
|
- Identify bottlenecks in launch sequence, memory allocations, CPU hotspots
|
||||||
|
|
||||||
|
### 5. Common Optimization Patterns
|
||||||
|
|
||||||
|
Based on successful optimizations:
|
||||||
|
|
||||||
|
- **Lazy initialization** - Defer expensive dependency resolution
|
||||||
|
- **Background operations** - Move non-critical setup off main thread
|
||||||
|
- **Deferred bindings** - Set up subscriptions after UI appears
|
||||||
|
- **Font loading** - Register fonts asynchronously
|
||||||
|
- **Method swizzling** - Only essential swizzles during launch
|
||||||
|
|
||||||
|
### Critical Pitfalls to Avoid
|
||||||
|
|
||||||
|
❌ Using device names instead of UUIDs
|
||||||
|
❌ Skipping clean installation step
|
||||||
|
❌ Building in Debug mode
|
||||||
|
❌ Trying to export trace immediately after recording
|
||||||
|
❌ Assuming --output path will be used
|
||||||
|
❌ Not waiting between installation and profiling
|
||||||
|
|
||||||
|
### Reference Documentation
|
||||||
|
|
||||||
|
See [examples](./examples.md) in this skill directory for:
|
||||||
|
|
||||||
|
- Detailed command examples with correct syntax
|
||||||
|
- Complete troubleshooting guide with solutions
|
||||||
|
- Real-world lessons learned
|
||||||
|
- Production-ready workflow scripts
|
||||||
687
skills/instruments/examples.md
Normal file
687
skills/instruments/examples.md
Normal file
@@ -0,0 +1,687 @@
|
|||||||
|
# XCTrace & Instruments Examples for iOS project
|
||||||
|
|
||||||
|
## Quick Reference - Working Commands ✅
|
||||||
|
|
||||||
|
These commands have been tested and verified to work correctly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get device UUID (always required)
|
||||||
|
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791" # iPhone 17 Pro
|
||||||
|
xcrun simctl list devices available | grep "iPhone"
|
||||||
|
|
||||||
|
# App Launch Profiling (most common use case)
|
||||||
|
xcrun simctl uninstall $DEVICE_UUID <bundle id>
|
||||||
|
xcrun simctl install $DEVICE_UUID /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app
|
||||||
|
sleep 2
|
||||||
|
xcrun xctrace record --template "App Launch" --device $DEVICE_UUID \
|
||||||
|
--launch -- /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app 2>&1
|
||||||
|
|
||||||
|
# Find generated trace file
|
||||||
|
ls -lt *.trace | head -1
|
||||||
|
|
||||||
|
# Open in Instruments for analysis
|
||||||
|
open -a Instruments.app Launch_<app name>*.trace
|
||||||
|
```
|
||||||
|
|
||||||
|
## XCTrace Overview
|
||||||
|
|
||||||
|
`xcrun xctrace` is the modern command-line interface for Instruments profiling. It replaces the deprecated `instruments` command and provides more reliable automation capabilities.
|
||||||
|
|
||||||
|
## Available Templates
|
||||||
|
|
||||||
|
Run `xcrun xctrace list templates` to see all available templates:
|
||||||
|
|
||||||
|
### Standard Templates
|
||||||
|
|
||||||
|
- **Activity Monitor**: General system activity monitoring
|
||||||
|
- **Allocations**: Memory allocation tracking
|
||||||
|
- **Animation Hitches**: UI animation performance issues
|
||||||
|
- **App Launch**: App startup performance analysis
|
||||||
|
- **Audio System Trace**: Audio subsystem performance
|
||||||
|
- **CPU Counters**: Hardware performance counters
|
||||||
|
- **CPU Profiler**: CPU usage profiling
|
||||||
|
- **Core ML**: Machine learning model performance
|
||||||
|
- **Data Persistence**: Core Data and file I/O analysis
|
||||||
|
- **File Activity**: File system operations
|
||||||
|
- **Game Memory**: Game-specific memory analysis
|
||||||
|
- **Game Performance**: Game-specific performance metrics
|
||||||
|
- **Game Performance Overview**: High-level game metrics
|
||||||
|
- **Leaks**: Memory leak detection
|
||||||
|
- **Logging**: System and app logging analysis
|
||||||
|
- **Metal System Trace**: GPU performance analysis
|
||||||
|
- **Network**: Network activity monitoring
|
||||||
|
- **Power Profiler**: Energy consumption analysis
|
||||||
|
- **Processor Trace**: Low-level CPU tracing
|
||||||
|
- **RealityKit Trace**: AR/VR performance analysis
|
||||||
|
- **Swift Concurrency**: Swift async/await performance
|
||||||
|
- **SwiftUI**: SwiftUI-specific performance analysis
|
||||||
|
- **System Trace**: System-wide performance analysis
|
||||||
|
- **Tailspin**: System responsiveness analysis
|
||||||
|
- **Time Profiler**: CPU time profiling
|
||||||
|
|
||||||
|
## Device Management
|
||||||
|
|
||||||
|
### List Available Devices
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all devices (physical and simulators)
|
||||||
|
xcrun xctrace list devices
|
||||||
|
|
||||||
|
# List only simulators with UUIDs (RECOMMENDED)
|
||||||
|
xcrun simctl list devices available | grep "iPhone"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current Available Devices for <app scheme>
|
||||||
|
|
||||||
|
- **Simulators**: iPhone 16 Pro, iPhone 16, iPhone 17 Pro (F464E766-555C-4B95-B8CC-763702A70791), iPad variants, etc.
|
||||||
|
|
||||||
|
### ⚠️ IMPORTANT: Always Use Device UUID, Not Name
|
||||||
|
|
||||||
|
Device names can be ambiguous. **Always use the device UUID** for reliable automation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ❌ WRONG - Ambiguous device name
|
||||||
|
xcrun xctrace record --template "App Launch" --device "iPhone 16 Pro" ...
|
||||||
|
|
||||||
|
# ✅ CORRECT - Use UUID
|
||||||
|
xcrun xctrace record --template "App Launch" --device F464E766-555C-4B95-B8CC-763702A70791 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic XCTrace Commands
|
||||||
|
|
||||||
|
### 1. App Launch Performance Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ CORRECT METHOD - Clean state, use UUID, proper app path handling
|
||||||
|
# Step 1: Get device UUID
|
||||||
|
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791" # iPhone 17 Pro
|
||||||
|
|
||||||
|
# Step 2: Clean up any existing app installations (IMPORTANT to avoid ambiguity)
|
||||||
|
xcrun simctl uninstall $DEVICE_UUID <bundle id>
|
||||||
|
|
||||||
|
# Step 3: Install fresh app build
|
||||||
|
xcrun simctl install $DEVICE_UUID /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app
|
||||||
|
|
||||||
|
# Step 4: Wait for installation to complete
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Step 5: Profile app launch
|
||||||
|
xcrun xctrace record --template "App Launch" \
|
||||||
|
--device $DEVICE_UUID \
|
||||||
|
--launch -- /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app \
|
||||||
|
--output ~/Desktop/<app name>_launch_analysis.trace 2>&1
|
||||||
|
|
||||||
|
# Note: Trace file will be saved with auto-generated name in current directory
|
||||||
|
# Example: Launch_<app name>.app_2025-10-30_3.55.40 PM_39E6A410.trace
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative: Complete Clean State (Most Reliable)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For most reliable results, completely erase and reboot simulator
|
||||||
|
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791"
|
||||||
|
|
||||||
|
xcrun simctl shutdown $DEVICE_UUID
|
||||||
|
xcrun simctl erase $DEVICE_UUID
|
||||||
|
xcrun simctl boot $DEVICE_UUID
|
||||||
|
xcrun simctl install $DEVICE_UUID /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app
|
||||||
|
sleep 2
|
||||||
|
xcrun xctrace record --template "App Launch" --device $DEVICE_UUID \
|
||||||
|
--launch -- /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app \
|
||||||
|
2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Memory Allocation Tracking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ Track memory allocations during app usage
|
||||||
|
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791"
|
||||||
|
|
||||||
|
# Option 1: Attach to running app
|
||||||
|
xcrun xctrace record --template "Allocations" \
|
||||||
|
--device $DEVICE_UUID \
|
||||||
|
--attach "<app name>" \
|
||||||
|
--time-limit 5m
|
||||||
|
|
||||||
|
# Option 2: Launch and track from start
|
||||||
|
xcrun xctrace record --template "Allocations" \
|
||||||
|
--device $DEVICE_UUID \
|
||||||
|
--launch -- /Users/daipham/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app \
|
||||||
|
--time-limit 5m
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. CPU Time Profiling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ Profile CPU usage
|
||||||
|
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791"
|
||||||
|
|
||||||
|
xcrun xctrace record --template "Time Profiler" \
|
||||||
|
--device $DEVICE_UUID \
|
||||||
|
--attach "<app name>" \
|
||||||
|
--time-limit 2m
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Memory Leak Detection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ Detect memory leaks
|
||||||
|
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791"
|
||||||
|
|
||||||
|
xcrun xctrace record --template "Leaks" \
|
||||||
|
--device $DEVICE_UUID \
|
||||||
|
--attach "<app name>" \
|
||||||
|
--time-limit 3m
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Network Activity Monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Monitor network requests and responses
|
||||||
|
xcrun xctrace record --template "Network" \
|
||||||
|
--device "iPhone 16 Pro Simulator (18.5)" \
|
||||||
|
--attach "<app name>" \
|
||||||
|
--output ~/Desktop/<app name>_network_analysis.trace \
|
||||||
|
--time-limit 5m
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. SwiftUI Performance Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analyze SwiftUI performance (if using SwiftUI)
|
||||||
|
xcrun xctrace record --template "SwiftUI" \
|
||||||
|
--device "iPhone 16 Pro Simulator (18.5)" \
|
||||||
|
--attach "<app name>" \
|
||||||
|
--output ~/Desktop/<app name>_swiftui_analysis.trace \
|
||||||
|
--time-limit 2m
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Animation Performance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Detect animation hitches and performance issues
|
||||||
|
xcrun xctrace record --template "Animation Hitches" \
|
||||||
|
--device "iPhone 16 Pro Simulator (18.5)" \
|
||||||
|
--attach "<app name>" \
|
||||||
|
--output ~/Desktop/<app name>_animation_analysis.trace \
|
||||||
|
--time-limit 3m
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Power/Energy Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analyze energy consumption
|
||||||
|
xcrun xctrace record --template "Power Profiler" \
|
||||||
|
--device "iPhone 16 Pro Simulator (18.5)" \
|
||||||
|
--attach "<app name>" \
|
||||||
|
--output ~/Desktop/<app name>_power_analysis.trace \
|
||||||
|
--time-limit 10m
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Recording All Processes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Record system-wide performance
|
||||||
|
xcrun xctrace record --template "System Trace" \
|
||||||
|
--device "iPhone 16 Pro Simulator (18.5)" \
|
||||||
|
--all-processes \
|
||||||
|
--output ~/Desktop/<app name>_system_trace.trace \
|
||||||
|
--time-limit 1m
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch with custom environment variables
|
||||||
|
xcrun xctrace record --template "Time Profiler" \
|
||||||
|
--device "iPhone 16 Pro Simulator (18.5)" \
|
||||||
|
--env "LOG_LEVEL=verbose" \
|
||||||
|
--launch -- "/path/to/<app name>.app" \
|
||||||
|
--output ~/Desktop/<app name>_debug_profile.trace \
|
||||||
|
--time-limit 2m
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Instruments
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Combine multiple instruments in one session
|
||||||
|
xcrun xctrace record \
|
||||||
|
--device "iPhone 16 Pro Simulator (18.5)" \
|
||||||
|
--instrument "Time Profiler" \
|
||||||
|
--instrument "Allocations" \
|
||||||
|
--instrument "Network" \
|
||||||
|
--attach "<app name>" \
|
||||||
|
--output ~/Desktop/<app name>_combined_analysis.trace \
|
||||||
|
--time-limit 3m
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Export and Analysis
|
||||||
|
|
||||||
|
### Table of Contents Export
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View available data in trace file
|
||||||
|
xcrun xctrace export --input ~/Desktop/<app name>_launch_analysis.trace --toc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Data Export
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export CPU profiling data
|
||||||
|
xcrun xctrace export --input ~/Desktop/<app name>_cpu_profile.trace \
|
||||||
|
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="time-profiler"]' \
|
||||||
|
--output ~/Desktop/cpu_data.xml
|
||||||
|
|
||||||
|
# Export memory allocation data
|
||||||
|
xcrun xctrace export --input ~/Desktop/<app name>_memory_analysis.trace \
|
||||||
|
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="allocations"]' \
|
||||||
|
--output ~/Desktop/memory_data.xml
|
||||||
|
|
||||||
|
# Export network data as HAR file
|
||||||
|
xcrun xctrace export --input ~/Desktop/<app name>_network_analysis.trace \
|
||||||
|
--har --output ~/Desktop/network_data.har
|
||||||
|
```
|
||||||
|
|
||||||
|
## iOS Project Specific Workflows
|
||||||
|
|
||||||
|
### Complete Performance Audit Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# <app name>_performance_audit.sh
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DEVICE="iPhone 16 Pro Simulator (18.5)"
|
||||||
|
APP_PATH="/Users/daipham/Library/Developer/Xcode/DerivedData/<app name>-demupeapxadrllglwxuahiesemhe/Build/Products/Release-iphonesimulator/<app name>.app"
|
||||||
|
OUTPUT_DIR="~/Desktop/<app name>_performance_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
BUNDLE_ID="<bundle id>"
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
echo "🚀 Starting <app scheme> Performance Audit..."
|
||||||
|
echo "📁 Results will be saved to: $OUTPUT_DIR"
|
||||||
|
|
||||||
|
# 1. App Launch Analysis
|
||||||
|
echo "📱 Analyzing app launch performance..."
|
||||||
|
xcrun xctrace record --template "App Launch" \
|
||||||
|
--device "$DEVICE" \
|
||||||
|
--launch -- "$APP_PATH" \
|
||||||
|
--output "$OUTPUT_DIR/launch_analysis.trace" \
|
||||||
|
--time-limit 30s
|
||||||
|
|
||||||
|
# 2. Memory Allocations
|
||||||
|
echo "🧠 Analyzing memory allocations..."
|
||||||
|
xcrun xctrace record --template "Allocations" \
|
||||||
|
--device "$DEVICE" \
|
||||||
|
--attach "$BUNDLE_ID" \
|
||||||
|
--output "$OUTPUT_DIR/memory_analysis.trace" \
|
||||||
|
--time-limit 2m
|
||||||
|
|
||||||
|
# 3. CPU Profiling
|
||||||
|
echo "⚡ Profiling CPU usage..."
|
||||||
|
xcrun xctrace record --template "Time Profiler" \
|
||||||
|
--device "$DEVICE" \
|
||||||
|
--attach "$BUNDLE_ID" \
|
||||||
|
--output "$OUTPUT_DIR/cpu_profile.trace" \
|
||||||
|
--time-limit 2m
|
||||||
|
|
||||||
|
# 4. Memory Leaks
|
||||||
|
echo "🔍 Checking for memory leaks..."
|
||||||
|
xcrun xctrace record --template "Leaks" \
|
||||||
|
--device "$DEVICE" \
|
||||||
|
--attach "$BUNDLE_ID" \
|
||||||
|
--output "$OUTPUT_DIR/leaks_analysis.trace" \
|
||||||
|
--time-limit 3m
|
||||||
|
|
||||||
|
# 5. Network Activity
|
||||||
|
echo "🌐 Monitoring network activity..."
|
||||||
|
xcrun xctrace record --template "Network" \
|
||||||
|
--device "$DEVICE" \
|
||||||
|
--attach "$BUNDLE_ID" \
|
||||||
|
--output "$OUTPUT_DIR/network_analysis.trace" \
|
||||||
|
--time-limit 3m
|
||||||
|
|
||||||
|
# 6. Animation Performance
|
||||||
|
echo "🎬 Analyzing animation performance..."
|
||||||
|
xcrun xctrace record --template "Animation Hitches" \
|
||||||
|
--device "$DEVICE" \
|
||||||
|
--attach "$BUNDLE_ID" \
|
||||||
|
--output "$OUTPUT_DIR/animation_analysis.trace" \
|
||||||
|
--time-limit 2m
|
||||||
|
|
||||||
|
echo "✅ Performance audit complete!"
|
||||||
|
echo "📂 Open traces in Instruments.app for detailed analysis"
|
||||||
|
echo "🔗 Trace files location: $OUTPUT_DIR"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Continuous Integration Performance Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# ci_performance_check.sh - For automated CI/CD pipelines
|
||||||
|
|
||||||
|
DEVICE="iPhone 16 Pro Simulator (18.5)"
|
||||||
|
APP_PATH="$1" # Pass app path as parameter
|
||||||
|
THRESHOLD_LAUNCH_TIME=3.0 # seconds
|
||||||
|
THRESHOLD_MEMORY_MB=150 # MB
|
||||||
|
|
||||||
|
# Quick launch time check
|
||||||
|
echo "⏱️ Measuring app launch time..."
|
||||||
|
xcrun xctrace record --template "App Launch" \
|
||||||
|
--device "$DEVICE" \
|
||||||
|
--launch -- "$APP_PATH" \
|
||||||
|
--output "launch_check.trace" \
|
||||||
|
--time-limit 10s
|
||||||
|
|
||||||
|
# Extract launch time (simplified - would need proper parsing)
|
||||||
|
# LAUNCH_TIME=$(parse_launch_time launch_check.trace)
|
||||||
|
|
||||||
|
# Memory footprint check
|
||||||
|
echo "💾 Measuring memory footprint..."
|
||||||
|
xcrun xctrace record --template "Allocations" \
|
||||||
|
--device "$DEVICE" \
|
||||||
|
--launch -- "$APP_PATH" \
|
||||||
|
--output "memory_check.trace" \
|
||||||
|
--time-limit 30s
|
||||||
|
|
||||||
|
echo "📊 Performance check complete"
|
||||||
|
# Add logic to fail CI if thresholds exceeded
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Workflow Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick development profiling
|
||||||
|
alias <app name>_profile='xcrun xctrace record --template "Time Profiler" --device "iPhone 16 Pro Simulator (18.5)" --attach "<app name>" --output ~/Desktop/quick_profile_$(date +%H%M%S).trace --time-limit 1m'
|
||||||
|
|
||||||
|
alias <app name>_memory='xcrun xctrace record --template "Allocations" --device "iPhone 16 Pro Simulator (18.5)" --attach "<app name>" --output ~/Desktop/memory_check_$(date +%H%M%S).trace --time-limit 2m'
|
||||||
|
|
||||||
|
alias <app name>_launch='xcrun xctrace record --template "App Launch" --device "iPhone 16 Pro Simulator (18.5)" --launch -- "/Users/daipham/Library/Developer/Xcode/DerivedData/<app name>-demupeapxadrllglwxuahiesemhe/Build/Products/Release-iphonesimulator/<app name>.app" --output ~/Desktop/launch_$(date +%H%M%S).trace --time-limit 20s'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips and Best Practices
|
||||||
|
|
||||||
|
### Device Selection
|
||||||
|
|
||||||
|
- Use physical devices for accurate performance data
|
||||||
|
- Simulators are good for development and debugging
|
||||||
|
- Match device to your target audience (iPhone vs iPad)
|
||||||
|
|
||||||
|
### Template Selection
|
||||||
|
|
||||||
|
- **App Launch**: Essential for user experience optimization
|
||||||
|
- **Time Profiler**: First choice for CPU performance issues
|
||||||
|
- **Allocations**: Critical for memory management
|
||||||
|
- **Leaks**: Important for long-term app stability
|
||||||
|
- **Network**: Essential for apps with API calls (like <app scheme>)
|
||||||
|
|
||||||
|
### Recording Duration
|
||||||
|
|
||||||
|
- App Launch: 15-30 seconds
|
||||||
|
- Memory Analysis: 2-5 minutes
|
||||||
|
- CPU Profiling: 1-3 minutes
|
||||||
|
- Leak Detection: 3-10 minutes (longer for thorough analysis)
|
||||||
|
|
||||||
|
### Automation Best Practices
|
||||||
|
|
||||||
|
- Always specify output paths to avoid conflicts
|
||||||
|
- Use timestamp-based file naming
|
||||||
|
- Set appropriate time limits to prevent infinite recording
|
||||||
|
- Consider device state (clean vs. with existing apps)
|
||||||
|
|
||||||
|
## Common XPath Expressions for Data Export
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CPU profiling data
|
||||||
|
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="time-profiler"]'
|
||||||
|
|
||||||
|
# Memory allocations
|
||||||
|
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="allocations"]'
|
||||||
|
|
||||||
|
# Network requests
|
||||||
|
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="network-connections"]'
|
||||||
|
|
||||||
|
# App launch metrics
|
||||||
|
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="app-launch"]'
|
||||||
|
|
||||||
|
# Animation hitches
|
||||||
|
--xpath '/trace-toc/run[@number="1"]/data/table[@schema="animation-hitches"]'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues and Solutions
|
||||||
|
|
||||||
|
#### 1. ❌ "Provided device parameter is ambiguous"
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun xctrace record --template "App Launch" --device "iPhone 17 Pro" ...
|
||||||
|
# Error: Provided device parameter 'iPhone 17 Pro' is ambiguous
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** Always use device UUID instead of name
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get UUID first
|
||||||
|
xcrun simctl list devices available | grep "iPhone 17 Pro"
|
||||||
|
# Output: iPhone 17 Pro (F464E766-555C-4B95-B8CC-763702A70791) (Booted)
|
||||||
|
|
||||||
|
# Use UUID in command
|
||||||
|
xcrun xctrace record --template "App Launch" --device F464E766-555C-4B95-B8CC-763702A70791 ....
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. ❌ "Provided process is ambiguous"
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Error: Provided process '/path/to/<app name>.app' is ambiguous
|
||||||
|
# /path1/<app name>.app
|
||||||
|
# /path2/<app name>.app
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root Cause:** Multiple app installations exist on the simulator (common after repeated builds)
|
||||||
|
|
||||||
|
**Solution:** Clean up before profiling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Method 1: Uninstall existing app
|
||||||
|
xcrun simctl uninstall $DEVICE_UUID <bundle id>
|
||||||
|
|
||||||
|
# Method 2: Erase entire simulator (nuclear option)
|
||||||
|
xcrun simctl shutdown $DEVICE_UUID
|
||||||
|
xcrun simctl erase $DEVICE_UUID
|
||||||
|
xcrun simctl boot $DEVICE_UUID
|
||||||
|
|
||||||
|
# Then reinstall fresh
|
||||||
|
xcrun simctl install $DEVICE_UUID /path/to/<app name>.app
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. ❌ "Export failed: Trace is malformed - run data is missing"
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun xctrace export --input trace.trace --toc
|
||||||
|
# Error: Export failed: Trace is malformed - run data is missing
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root Cause:** Trace file is still being written or profiling was interrupted
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
- Wait for profiling to fully complete (look for "Output file saved as:" message)
|
||||||
|
- Don't try to export immediately after xctrace record completes
|
||||||
|
- Some traces may not export properly via CLI - open in Instruments.app instead
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if profiling completed
|
||||||
|
ls -la *.trace # Look for complete directory structure
|
||||||
|
|
||||||
|
# Open in Instruments for analysis instead
|
||||||
|
open -a Instruments.app trace.trace
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. ❌ "No such file or directory" with paths containing spaces
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun xctrace record --input "/path with spaces/file.trace" ...
|
||||||
|
# Error: File does not exist at path
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** Use environment variables or proper quoting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Method 1: Environment variable (RECOMMENDED)
|
||||||
|
TRACE_FILE="/path with spaces/file.trace"
|
||||||
|
xcrun xctrace export --input "$TRACE_FILE" --toc
|
||||||
|
|
||||||
|
# Method 2: Escape properly
|
||||||
|
xcrun xctrace export --input "/path\ with\ spaces/file.trace" --toc
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. ❌ Trace file saved in wrong location
|
||||||
|
|
||||||
|
**Problem:** Specified `--output ~/Desktop/trace.trace` but file appears in current directory
|
||||||
|
|
||||||
|
**Root Cause:** xctrace may ignore --output path and auto-generate filename
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trace files are saved with auto-generated names like:
|
||||||
|
# Launch_<app name>.app_2025-10-30_3.55.40 PM_39E6A410.trace
|
||||||
|
|
||||||
|
# Find your trace file
|
||||||
|
find . -name "*.trace" -type d -mmin -5 # Files modified in last 5 minutes
|
||||||
|
|
||||||
|
# Or look for specific pattern
|
||||||
|
ls -lt *.trace | head -1 # Most recent trace
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. ❌ App Launch template not capturing data
|
||||||
|
|
||||||
|
**Problem:** Trace file created but no meaningful data
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
- Ensure app actually launches (check simulator)
|
||||||
|
- Build app in Release configuration for realistic performance
|
||||||
|
- Don't specify --time-limit too short (use at least 10-15 seconds)
|
||||||
|
- Check that device is booted before profiling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify device is booted
|
||||||
|
xcrun simctl list devices | grep Booted
|
||||||
|
|
||||||
|
# Boot if needed
|
||||||
|
xcrun simctl boot $DEVICE_UUID
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all available devices with UUIDs
|
||||||
|
xcrun simctl list devices available
|
||||||
|
|
||||||
|
# Check if app is installed
|
||||||
|
xcrun simctl listapps $DEVICE_UUID | grep <app name>
|
||||||
|
|
||||||
|
# Verbose output
|
||||||
|
xcrun xctrace record --template "Time Profiler" --device "$DEVICE_UUID" --time-limit 30s --verbose
|
||||||
|
|
||||||
|
# Check available instruments
|
||||||
|
xcrun xctrace list instruments
|
||||||
|
|
||||||
|
# Validate trace file structure
|
||||||
|
ls -la trace.trace/
|
||||||
|
|
||||||
|
# Open trace in Instruments GUI (most reliable for analysis)
|
||||||
|
open -a Instruments.app trace.trace
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices to Avoid Issues
|
||||||
|
|
||||||
|
1. **Always use device UUID** - Never use device names
|
||||||
|
2. **Clean state before profiling** - Uninstall app or erase simulator first
|
||||||
|
3. **Use environment variables** - For paths with spaces or complex names
|
||||||
|
4. **Wait between steps** - Add `sleep 2` after installation before profiling
|
||||||
|
5. **Build in Release mode** - For accurate performance measurements
|
||||||
|
6. **Check output location** - Trace files may have auto-generated names
|
||||||
|
7. **Use Instruments.app** - For final analysis when CLI export fails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned from Real-World Usage (2025-10-30)
|
||||||
|
|
||||||
|
### What Worked ✅
|
||||||
|
|
||||||
|
1. **Device UUID approach**: Using `xcrun simctl list devices available | grep "iPhone"` to get UUID, then using UUID in all commands
|
||||||
|
2. **Clean installation**: Running `xcrun simctl uninstall` before each profiling session eliminated ambiguity errors
|
||||||
|
3. **Environment variables**: Using `DEVICE_UUID="..."` made commands more reliable with special characters
|
||||||
|
4. **Background execution**: Using `2>&1` redirect and `run_in_background` for long-running profiles
|
||||||
|
5. **Auto-generated filenames**: Accepting that xctrace generates its own filenames (e.g., `Launch_<app name>.app_2025-10-30_3.55.40 PM_39E6A410.trace`)
|
||||||
|
|
||||||
|
### What Didn't Work ❌
|
||||||
|
|
||||||
|
1. **Device names**: `--device "iPhone 17 Pro"` or `--device "iPhone 16 Pro Simulator (18.5)"` - Always ambiguous
|
||||||
|
2. **Direct export**: `xcrun xctrace export --input trace.trace --toc` often failed with "malformed trace" errors
|
||||||
|
3. **Absolute --output paths**: Often ignored by xctrace, files saved with auto-generated names instead
|
||||||
|
4. **Multiple app installations**: Caused "process is ambiguous" errors - must clean up first
|
||||||
|
5. **Immediate export**: Trying to export trace immediately after recording before file fully written
|
||||||
|
|
||||||
|
### Performance Improvements Achieved
|
||||||
|
|
||||||
|
From this profiling session, we identified and fixed these launch time issues in <app name> app:
|
||||||
|
|
||||||
|
1. **Lazy ViewModel initialization**: Deferred 7 use case resolutions from eager to lazy loading
|
||||||
|
2. **Deferred swizzling**: Moved 4 tracking swizzle operations to background thread
|
||||||
|
3. **Background font loading**: Moved `FontFamily.registerAllCustomFonts()` off main thread
|
||||||
|
4. **Deferred RxSwift bindings**: Moved subscription setup to next run loop iteration
|
||||||
|
|
||||||
|
**Expected improvement**: 40-60% reduction in main thread blocking during launch
|
||||||
|
|
||||||
|
### Recommended Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Proven workflow for app launch profiling
|
||||||
|
|
||||||
|
# 1. Setup
|
||||||
|
DEVICE_UUID="F464E766-555C-4B95-B8CC-763702A70791"
|
||||||
|
APP_PATH="/Users/<username>/Library/Developer/Xcode/DerivedData/<app name>/Build/Products/Release-iphonesimulator/<app name>.app"
|
||||||
|
|
||||||
|
# 2. Build in Release
|
||||||
|
xcodebuild -workspace <app name>.xcworkspace \
|
||||||
|
-scheme "<app scheme>" \
|
||||||
|
-configuration Release \
|
||||||
|
-sdk iphonesimulator \
|
||||||
|
-derivedDataPath ~/Library/Developer/Xcode/DerivedData/<app name> \
|
||||||
|
build
|
||||||
|
|
||||||
|
# 3. Clean simulator state
|
||||||
|
xcrun simctl uninstall $DEVICE_UUID <bundle id>
|
||||||
|
|
||||||
|
# 4. Install fresh
|
||||||
|
xcrun simctl install $DEVICE_UUID "$APP_PATH"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 5. Profile
|
||||||
|
xcrun xctrace record --template "App Launch" \
|
||||||
|
--device $DEVICE_UUID \
|
||||||
|
--launch -- "$APP_PATH" 2>&1
|
||||||
|
|
||||||
|
# 6. Find and open trace
|
||||||
|
TRACE=$(ls -t Launch_<app name>*.trace 2>/dev/null | head -1)
|
||||||
|
echo "Trace saved to: $TRACE"
|
||||||
|
open -a Instruments.app "$TRACE"
|
||||||
|
```
|
||||||
|
|
||||||
|
This workflow has been tested and verified to work reliably.
|
||||||
157
skills/ios-code-review/SKILL.md
Normal file
157
skills/ios-code-review/SKILL.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
---
|
||||||
|
name: ios-code-review
|
||||||
|
description: Comprehensive iOS Swift code review for Payoo Merchant app. Checks RxSwift patterns, Clean Architecture, naming conventions, memory management, security, and performance. Use when reviewing Swift files, pull requests, or ViewModels, ViewControllers, UseCases, and Repositories.
|
||||||
|
allowed-tools: Read, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# iOS Code Review
|
||||||
|
|
||||||
|
Expert iOS code reviewer for Payoo Merchant application, specializing in Swift, RxSwift reactive programming, and Clean Architecture patterns.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- "review code", "check this file", "review PR"
|
||||||
|
- Mentions Swift files: ViewController, ViewModel, UseCase, Repository
|
||||||
|
- "code quality", "best practices", "check standards"
|
||||||
|
- RxSwift, Clean Architecture, MVVM patterns
|
||||||
|
|
||||||
|
## Review Process
|
||||||
|
|
||||||
|
### Step 1: Identify Scope
|
||||||
|
Determine what to review:
|
||||||
|
- Specific files (e.g., "PaymentViewModel.swift")
|
||||||
|
- Directories (e.g., "Payment module")
|
||||||
|
- Git changes (recent commits, PR diff)
|
||||||
|
- Entire module or feature
|
||||||
|
|
||||||
|
### Step 2: Read and Analyze
|
||||||
|
Use Read tool to examine files, checking against 6 core categories.
|
||||||
|
|
||||||
|
### Step 3: Apply Standards
|
||||||
|
|
||||||
|
#### 1. Naming Conventions ✅
|
||||||
|
- **Types**: PascalCase, descriptive (e.g., `PaymentViewModel`)
|
||||||
|
- **Variables**: camelCase (e.g., `paymentAmount`, `isLoading`)
|
||||||
|
- **Booleans**: Prefix with `is`, `has`, `should`, `can`
|
||||||
|
- **No abbreviations** except URL, ID, VC, UC
|
||||||
|
- **IBOutlets**: Include type suffix (e.g., `amountTextField`)
|
||||||
|
|
||||||
|
#### 2. RxSwift Patterns 🔄
|
||||||
|
- **Disposal**: Every `.subscribe()` has `.disposed(by: disposeBag)`
|
||||||
|
- **Memory**: Use `[weak self]` in closures
|
||||||
|
- **Schedulers**: `subscribeOn(background)` for work, `observeOn(main)` for UI
|
||||||
|
- **Errors**: All chains handle errors
|
||||||
|
- **Relays**: Use `BehaviorRelay` not `BehaviorSubject`
|
||||||
|
|
||||||
|
#### 3. Clean Architecture 🏗️
|
||||||
|
- **Flow**: ViewModel → UseCase → Repository → API/DB
|
||||||
|
- **ViewModels**: Extend `BaseViewModel<State>`, no business logic
|
||||||
|
- **UseCases**: Contain all business logic
|
||||||
|
- **DI**: Dependencies injected via constructor (Swinject)
|
||||||
|
|
||||||
|
#### 4. Security 🔒
|
||||||
|
- Payment data in Keychain, never UserDefaults
|
||||||
|
- No sensitive data in logs
|
||||||
|
- HTTPS with certificate pinning
|
||||||
|
- Input validation on amounts
|
||||||
|
|
||||||
|
#### 5. UI/UX 🎨
|
||||||
|
- Simple titles: Use `title` property
|
||||||
|
- Complex titles: Use `navigationItem.titleView` only when subtitle exists
|
||||||
|
- Accessibility labels and hints
|
||||||
|
- Loading states with feedback
|
||||||
|
|
||||||
|
#### 6. Performance ⚡
|
||||||
|
- Database ops on background threads
|
||||||
|
- No retain cycles
|
||||||
|
- Image caching
|
||||||
|
- Proper memory management
|
||||||
|
|
||||||
|
### Step 4: Generate Report
|
||||||
|
|
||||||
|
Provide structured output with:
|
||||||
|
- **Summary**: Issue counts by severity (🔴 Critical, 🟠 High, 🟡 Medium, 🟢 Low)
|
||||||
|
- **Issues by category**: Organized findings
|
||||||
|
- **Code examples**: Current vs. fixed code
|
||||||
|
- **Explanations**: Why it matters
|
||||||
|
- **Recommendations**: Prioritized actions
|
||||||
|
|
||||||
|
## Severity Levels
|
||||||
|
|
||||||
|
🔴 **Critical** - Fix immediately
|
||||||
|
- Missing `.disposed(by: disposeBag)` → Memory leak
|
||||||
|
- Strong `self` references → Retain cycle
|
||||||
|
- Payment data in UserDefaults → Security risk
|
||||||
|
- UI updates off main thread → Crash risk
|
||||||
|
|
||||||
|
🟠 **High Priority** - Fix soon
|
||||||
|
- No error handling in Observable chains
|
||||||
|
- Wrong scheduler usage
|
||||||
|
- ViewModel calling API directly
|
||||||
|
- Business logic in ViewModel
|
||||||
|
|
||||||
|
🟡 **Medium Priority** - Should improve
|
||||||
|
- Using deprecated `BehaviorSubject`
|
||||||
|
- Poor naming (abbreviations)
|
||||||
|
- Missing accessibility labels
|
||||||
|
|
||||||
|
🟢 **Low Priority** - Nice to have
|
||||||
|
- Inconsistent style
|
||||||
|
- Could be more descriptive
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# iOS Code Review Report
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- 🔴 Critical: X | 🟠 High: X | 🟡 Medium: X | 🟢 Low: X
|
||||||
|
- By category: Naming: X, RxSwift: X, Architecture: X, etc.
|
||||||
|
|
||||||
|
## Critical Issues
|
||||||
|
|
||||||
|
### 🔴 [Category] - [Issue Title]
|
||||||
|
**File**: `path/to/file.swift:line`
|
||||||
|
|
||||||
|
**Current**:
|
||||||
|
```swift
|
||||||
|
// problematic code
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
```swift
|
||||||
|
// corrected code
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: [Explanation of impact]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
1. Fix all critical issues immediately
|
||||||
|
2. Address high priority before next release
|
||||||
|
3. Plan medium priority for next sprint
|
||||||
|
|
||||||
|
## Positive Observations
|
||||||
|
✅ [Acknowledge well-written code]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Standards**: `.github/instructions/ios-merchant-code-review.instructions.md`
|
||||||
|
- Lines 36-393: Naming Conventions
|
||||||
|
- Lines 410-613: RxSwift Patterns
|
||||||
|
- Lines 615-787: Architecture
|
||||||
|
- Lines 789-898: Security
|
||||||
|
- Lines 1181-1288: Testing
|
||||||
|
- Lines 1363-1428: Performance
|
||||||
|
|
||||||
|
**Detailed Examples**: See `examples.md` in this skill directory for extensive code examples and patterns.
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- **Be thorough**: Check all 6 categories
|
||||||
|
- **Be specific**: Reference exact line numbers
|
||||||
|
- **Be constructive**: Explain why, not just what
|
||||||
|
- **Be practical**: Prioritize by severity
|
||||||
|
- **Be encouraging**: Acknowledge good code
|
||||||
637
skills/ios-code-review/examples.md
Normal file
637
skills/ios-code-review/examples.md
Normal file
@@ -0,0 +1,637 @@
|
|||||||
|
# iOS Code Review Examples
|
||||||
|
|
||||||
|
Detailed examples for each review category with good/bad patterns.
|
||||||
|
|
||||||
|
## 1. Naming Conventions Examples
|
||||||
|
|
||||||
|
### ✅ Good Naming
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Classes and Types
|
||||||
|
class PaymentViewController: UIViewController { }
|
||||||
|
class RefundRequestViewModel: BaseViewModel<RefundRequestState> { }
|
||||||
|
protocol PaymentUseCase { }
|
||||||
|
|
||||||
|
// Variables and Properties
|
||||||
|
let paymentAmount = BehaviorRelay<String>(value: "")
|
||||||
|
let isProcessingPayment = BehaviorRelay<Bool>(value: false)
|
||||||
|
let transactions = BehaviorRelay<[Transaction]>(value: [])
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
func loadTransactions() { }
|
||||||
|
func processPaymentRequest(amount: Double) { }
|
||||||
|
func validatePaymentAmount(_ amount: String) -> Bool { }
|
||||||
|
|
||||||
|
// IBOutlets
|
||||||
|
@IBOutlet weak var paymentAmountTextField: UITextField!
|
||||||
|
@IBOutlet weak var confirmButton: UIButton!
|
||||||
|
@IBOutlet weak var transactionTableView: UITableView!
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Bad Naming
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Classes - Too abbreviated or generic
|
||||||
|
class PayVC: UIViewController { } // What is "Pay"?
|
||||||
|
class RefReqVM { } // Too abbreviated
|
||||||
|
class Manager { } // Too generic
|
||||||
|
|
||||||
|
// Variables - Unclear or abbreviated
|
||||||
|
let amt = BehaviorRelay<String>(value: "") // What is "amt"?
|
||||||
|
let flag = BehaviorRelay<Bool>(value: false) // Meaningless
|
||||||
|
let data = BehaviorRelay<[Any]>(value: []) // Too generic
|
||||||
|
|
||||||
|
// Functions - Vague
|
||||||
|
func doSomething() { } // What does it do?
|
||||||
|
func process() { } // Process what?
|
||||||
|
func handle() { } // Handle what?
|
||||||
|
|
||||||
|
// IBOutlets - Missing type suffix
|
||||||
|
@IBOutlet weak var amount: UITextField! // Should be amountTextField
|
||||||
|
@IBOutlet weak var btn: UIButton! // Should be confirmButton
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. RxSwift Pattern Examples
|
||||||
|
|
||||||
|
### ✅ Proper RxSwift Usage
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
private let paymentUC: PaymentUseCase
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
let paymentAmount = BehaviorRelay<String>(value: "")
|
||||||
|
let isProcessing = BehaviorRelay<Bool>(value: false)
|
||||||
|
|
||||||
|
init(paymentUC: PaymentUseCase) {
|
||||||
|
self.paymentUC = paymentUC
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func processPayment() {
|
||||||
|
guard !paymentAmount.value.isEmpty else {
|
||||||
|
setState(.showError(PaymentError.invalidAmount))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessing.accept(true)
|
||||||
|
|
||||||
|
paymentUC.execute(amount: paymentAmount.value)
|
||||||
|
.subscribeOn(ConcurrentScheduler.background)
|
||||||
|
.observeOn(MainScheduler.instance)
|
||||||
|
.subscribe(
|
||||||
|
onNext: { [weak self] result in
|
||||||
|
self?.handleSuccess(result)
|
||||||
|
},
|
||||||
|
onError: { [weak self] error in
|
||||||
|
self?.handleError(error)
|
||||||
|
},
|
||||||
|
onCompleted: { [weak self] in
|
||||||
|
self?.isProcessing.accept(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.disposed(by: disposeBag) // ✓ Proper disposal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Common RxSwift Mistakes
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
// ❌ Missing DisposeBag property
|
||||||
|
|
||||||
|
func processPayment() {
|
||||||
|
// ❌ MEMORY LEAK: No disposal
|
||||||
|
paymentUC.execute(amount: paymentAmount.value)
|
||||||
|
.subscribe(onNext: { result in
|
||||||
|
// ❌ RETAIN CYCLE: Strong self reference
|
||||||
|
self.handleSuccess(result)
|
||||||
|
})
|
||||||
|
// MISSING: .disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadData() {
|
||||||
|
// ❌ Wrong scheduler for UI updates
|
||||||
|
networkService.fetchData()
|
||||||
|
.subscribeOn(MainScheduler.instance) // Wrong!
|
||||||
|
.subscribe(onNext: { data in
|
||||||
|
self.tableView.reloadData() // May be on background thread
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshData() {
|
||||||
|
// ❌ No error handling
|
||||||
|
dataSource.getData()
|
||||||
|
.subscribe(onNext: { data in
|
||||||
|
// Handle data
|
||||||
|
})
|
||||||
|
// MISSING: onError handler
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DisposeBag Anti-Patterns
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ BAD: Local DisposeBag
|
||||||
|
func loadData() {
|
||||||
|
let disposeBag = DisposeBag() // Local variable!
|
||||||
|
|
||||||
|
api.fetchData()
|
||||||
|
.subscribe(onNext: { data in
|
||||||
|
// Handle data
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
// DisposeBag deallocates here, cancels subscription immediately!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Multiple DisposeBags
|
||||||
|
class ViewModel {
|
||||||
|
private var searchDisposeBag = DisposeBag() // Anti-pattern
|
||||||
|
private var dataDisposeBag = DisposeBag() // Anti-pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Single DisposeBag property
|
||||||
|
class ViewModel {
|
||||||
|
private let disposeBag = DisposeBag() // Correct!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Clean Architecture Examples
|
||||||
|
|
||||||
|
### ✅ Proper Layer Separation
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// PRESENTATION LAYER - ViewModel
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
private let paymentUC: PaymentUseCase // ✓ Uses UseCase
|
||||||
|
|
||||||
|
init(paymentUC: PaymentUseCase) {
|
||||||
|
self.paymentUC = paymentUC
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func processPayment(amount: String) {
|
||||||
|
paymentUC.execute(amount: amount) // ✓ Delegates to UseCase
|
||||||
|
.subscribe(
|
||||||
|
onNext: { [weak self] result in
|
||||||
|
self?.setState(.success(result))
|
||||||
|
},
|
||||||
|
onError: { [weak self] error in
|
||||||
|
self?.setState(.error(error))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOMAIN LAYER - UseCase
|
||||||
|
protocol PaymentUseCase {
|
||||||
|
func execute(amount: String) -> Single<PaymentResult>
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentUseCaseImpl: PaymentUseCase {
|
||||||
|
private let paymentRepository: PaymentRepository
|
||||||
|
private let validationService: ValidationService
|
||||||
|
|
||||||
|
init(paymentRepository: PaymentRepository,
|
||||||
|
validationService: ValidationService) {
|
||||||
|
self.paymentRepository = paymentRepository
|
||||||
|
self.validationService = validationService
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(amount: String) -> Single<PaymentResult> {
|
||||||
|
// ✓ Business logic in UseCase
|
||||||
|
return validationService.validateAmount(amount)
|
||||||
|
.flatMap { validAmount in
|
||||||
|
return self.paymentRepository.processPayment(amount: validAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DATA LAYER - Repository
|
||||||
|
protocol PaymentRepository {
|
||||||
|
func processPayment(amount: Double) -> Single<PaymentResult>
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentRepositoryImpl: PaymentRepository {
|
||||||
|
private let apiService: PaymentApiService
|
||||||
|
private let localStorage: PaymentLocalStorage
|
||||||
|
|
||||||
|
func processPayment(amount: Double) -> Single<PaymentResult> {
|
||||||
|
return apiService.processPayment(amount: amount)
|
||||||
|
.do(onSuccess: { [weak self] result in
|
||||||
|
self?.localStorage.savePaymentRecord(result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Layer Violations
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ BAD: ViewModel bypassing UseCase
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
private let apiService: PaymentApiService // ❌ Wrong layer!
|
||||||
|
|
||||||
|
func processPayment() {
|
||||||
|
// ❌ Direct API call, no business logic
|
||||||
|
apiService.processPayment(amount: amount)
|
||||||
|
.subscribe(onNext: { result in
|
||||||
|
// ❌ Direct storage access
|
||||||
|
RealmManager.shared.save(result)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Business logic in ViewModel
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
func processPayment(amount: Double) {
|
||||||
|
// ❌ Validation logic in ViewModel
|
||||||
|
guard amount > 1000 else { return }
|
||||||
|
guard amount < 50_000_000 else { return }
|
||||||
|
|
||||||
|
// ❌ Business rules in ViewModel
|
||||||
|
let fee = amount * 0.01
|
||||||
|
let total = amount + fee
|
||||||
|
|
||||||
|
// This should all be in UseCase!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Direct instantiation
|
||||||
|
class PaymentViewController: UIViewController {
|
||||||
|
// ❌ Hard-coded dependencies
|
||||||
|
private let viewModel = PaymentViewModel(
|
||||||
|
paymentUC: PaymentUseCaseImpl() // ❌ Direct instantiation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Security Examples
|
||||||
|
|
||||||
|
### ✅ Secure Payment Handling
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class PaymentSecurityManager {
|
||||||
|
private let keychain = KeychainWrapper.standard
|
||||||
|
|
||||||
|
// ✓ Store in Keychain
|
||||||
|
func storePaymentToken(_ token: String, for merchantId: String) {
|
||||||
|
let key = "payment_token_\(merchantId)"
|
||||||
|
keychain.set(token, forKey: key,
|
||||||
|
withAccessibility: .whenUnlockedThisDeviceOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
func retrievePaymentToken(for merchantId: String) -> String? {
|
||||||
|
let key = "payment_token_\(merchantId)"
|
||||||
|
return keychain.string(forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✓ Mask sensitive data in logs
|
||||||
|
class PaymentRequest {
|
||||||
|
let amount: Double
|
||||||
|
let merchantId: String
|
||||||
|
|
||||||
|
override var description: String {
|
||||||
|
return "PaymentRequest(amount: \(amount), merchantId: ***)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✓ HTTPS with certificate pinning
|
||||||
|
class PaymentNetworkManager {
|
||||||
|
private lazy var session: URLSession = {
|
||||||
|
let config = URLSessionConfiguration.default
|
||||||
|
return URLSession(
|
||||||
|
configuration: config,
|
||||||
|
delegate: CertificatePinningDelegate(),
|
||||||
|
delegateQueue: nil
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Security Violations
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ BAD: Insecure storage
|
||||||
|
class PaymentManager {
|
||||||
|
func storePaymentToken(_ token: String) {
|
||||||
|
// ❌ UserDefaults is not secure!
|
||||||
|
UserDefaults.standard.set(token, forKey: "payment_token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func processPayment(_ request: PaymentRequest) {
|
||||||
|
// ❌ Logging sensitive data!
|
||||||
|
print("Processing payment: \(request)")
|
||||||
|
print("Card number: \(request.cardNumber)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: No certificate pinning
|
||||||
|
class PaymentNetworkManager {
|
||||||
|
func processPayment(_ request: PaymentRequest) {
|
||||||
|
let url = URL(string: "http://api.payoo.vn/payment")! // ❌ HTTP!
|
||||||
|
|
||||||
|
// ❌ No encryption
|
||||||
|
let data = try! JSONEncoder().encode(request)
|
||||||
|
|
||||||
|
// ❌ No certificate pinning
|
||||||
|
URLSession.shared.dataTask(with: url) { _, _, _ in }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI/UX Examples
|
||||||
|
|
||||||
|
### ✅ Proper Navigation Setup
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ✓ Simple title
|
||||||
|
class PaymentViewController: UIViewController {
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
title = "Payment" // ✓ Use title property
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✓ Title with subtitle (only when subtitle exists)
|
||||||
|
class StoreSelectionViewController: UIViewController {
|
||||||
|
private let titleDescription: String?
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
if let description = titleDescription, !description.isEmpty {
|
||||||
|
// ✓ Only use titleView when subtitle exists
|
||||||
|
navigationItem.titleView = createTitleView(
|
||||||
|
title: "Store Selection",
|
||||||
|
description: description
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// ✓ Use simple title when no subtitle
|
||||||
|
title = "Store Selection"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✓ Loading states with feedback
|
||||||
|
class QRSaleViewController: UIViewController {
|
||||||
|
private func bindLoadingStates() {
|
||||||
|
viewModel.isProcessing
|
||||||
|
.bind(to: loadingIndicator.rx.isAnimating)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
viewModel.isProcessing
|
||||||
|
.map { !$0 }
|
||||||
|
.bind(to: processButton.rx.isEnabled)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
viewModel.isProcessing
|
||||||
|
.map { $0 ? "Processing..." : "Process Payment" }
|
||||||
|
.bind(to: processButton.rx.title(for: .normal))
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✓ Accessibility
|
||||||
|
class PaymentAmountView: UIView {
|
||||||
|
private func setupAccessibility() {
|
||||||
|
amountTextField.isAccessibilityElement = true
|
||||||
|
amountTextField.accessibilityLabel = "Payment amount"
|
||||||
|
amountTextField.accessibilityHint = "Enter the payment amount in VND"
|
||||||
|
|
||||||
|
// ✓ Dynamic Type support
|
||||||
|
amountTextField.font = UIFont.preferredFont(forTextStyle: .title2)
|
||||||
|
amountTextField.adjustsFontForContentSizeCategory = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ UI/UX Issues
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ BAD: Using titleView for simple title
|
||||||
|
class PaymentViewController: UIViewController {
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// ❌ Unnecessary custom title view
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = "Payment"
|
||||||
|
navigationItem.titleView = titleLabel // Should use title property!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: No loading feedback
|
||||||
|
class QRSaleViewController: UIViewController {
|
||||||
|
private func processPayment() {
|
||||||
|
// ❌ No loading indicator
|
||||||
|
viewModel.processPayment() // User has no feedback!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: No accessibility
|
||||||
|
class PaymentAmountView: UIView {
|
||||||
|
// ❌ No accessibility setup
|
||||||
|
// ❌ No Dynamic Type support
|
||||||
|
// ❌ Missing accessibility labels
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Performance Examples
|
||||||
|
|
||||||
|
### ✅ Proper Memory Management
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class ImageDownloadManager {
|
||||||
|
private let cache = NSCache<NSString, UIImage>()
|
||||||
|
private var activeDownloads: [String: Disposable] = [:]
|
||||||
|
|
||||||
|
func downloadImage(from url: String) -> Observable<UIImage> {
|
||||||
|
let cacheKey = url as NSString
|
||||||
|
|
||||||
|
// ✓ Check cache first
|
||||||
|
if let cachedImage = cache.object(forKey: cacheKey) {
|
||||||
|
return .just(cachedImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✓ Cancel existing download
|
||||||
|
activeDownloads[url]?.dispose()
|
||||||
|
|
||||||
|
let download = URLSession.shared.rx
|
||||||
|
.data(request: URLRequest(url: URL(string: url)!))
|
||||||
|
.compactMap { UIImage(data: $0) }
|
||||||
|
.do(
|
||||||
|
onNext: { [weak self] image in
|
||||||
|
self?.cache.setObject(image, forKey: cacheKey)
|
||||||
|
},
|
||||||
|
onDispose: { [weak self] in
|
||||||
|
self?.activeDownloads.removeValue(forKey: url)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.share(replay: 1, scope: .whileConnected)
|
||||||
|
|
||||||
|
activeDownloads[url] = download.connect()
|
||||||
|
return download
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✓ Database on background thread
|
||||||
|
class TransactionRepository {
|
||||||
|
func getTransactions() -> Observable<[Transaction]> {
|
||||||
|
return Observable.collection(from: realm.objects(TransactionObject.self))
|
||||||
|
.map { results in
|
||||||
|
return results.map { Transaction(from: $0) }
|
||||||
|
}
|
||||||
|
.subscribeOn(ConcurrentScheduler.background) // ✓ Background
|
||||||
|
.observeOn(MainScheduler.instance) // ✓ Main for UI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Performance Issues
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ BAD: Memory leak from strong references
|
||||||
|
class ImageDownloadManager {
|
||||||
|
private var downloads: [URLSessionDataTask] = [] // ❌ Strong references
|
||||||
|
|
||||||
|
func downloadImage(from url: String) -> Observable<UIImage> {
|
||||||
|
return Observable.create { observer in
|
||||||
|
let task = URLSession.shared.dataTask(with: URL(string: url)!) { data, _, _ in
|
||||||
|
// Process
|
||||||
|
}
|
||||||
|
|
||||||
|
self.downloads.append(task) // ❌ Never removed - leak!
|
||||||
|
task.resume()
|
||||||
|
|
||||||
|
return Disposables.create {
|
||||||
|
task.cancel()
|
||||||
|
// ❌ Still in downloads array!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Blocking main thread
|
||||||
|
class TransactionRepository {
|
||||||
|
func getTransactions() -> Observable<[Transaction]> {
|
||||||
|
return Observable.create { observer in
|
||||||
|
// ❌ Blocking operation on main thread!
|
||||||
|
let realm = try! Realm()
|
||||||
|
let results = realm.objects(TransactionObject.self)
|
||||||
|
let transactions = results.map { Transaction(from: $0) }
|
||||||
|
|
||||||
|
observer.onNext(Array(transactions))
|
||||||
|
observer.onCompleted()
|
||||||
|
|
||||||
|
return Disposables.create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Review Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: New Feature Review
|
||||||
|
|
||||||
|
**Code**: New payment processing feature
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. Naming: All classes/variables descriptive?
|
||||||
|
2. RxSwift: Disposal and memory management?
|
||||||
|
3. Architecture: Proper layer separation?
|
||||||
|
4. Security: Payment data handled securely?
|
||||||
|
5. Tests: Unit tests included?
|
||||||
|
6. Performance: No blocking operations?
|
||||||
|
|
||||||
|
### Scenario 2: Bug Fix Review
|
||||||
|
|
||||||
|
**Code**: Fix for crash in transaction list
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. Root cause addressed?
|
||||||
|
2. No force unwrapping?
|
||||||
|
3. Proper error handling added?
|
||||||
|
4. Tests for the bug scenario?
|
||||||
|
5. No new issues introduced?
|
||||||
|
|
||||||
|
### Scenario 3: Refactoring Review
|
||||||
|
|
||||||
|
**Code**: Refactor ViewModel to use Clean Architecture
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. UseCase layer added?
|
||||||
|
2. Business logic moved from ViewModel?
|
||||||
|
3. DI setup correctly?
|
||||||
|
4. Tests still pass?
|
||||||
|
5. No breaking changes?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Detailed Examples**: See `examples.md` for extensive code samples and scenarios.
|
||||||
|
|
||||||
|
## Quick Reference Checklist
|
||||||
|
|
||||||
|
Copy this for quick reviews:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
### Naming ✅
|
||||||
|
- [ ] Classes: PascalCase, descriptive
|
||||||
|
- [ ] Variables: camelCase, meaningful
|
||||||
|
- [ ] Booleans: is/has/should/can prefix
|
||||||
|
- [ ] No abbreviations
|
||||||
|
- [ ] IBOutlets: type suffix
|
||||||
|
|
||||||
|
### RxSwift 🔄
|
||||||
|
- [ ] All subscriptions disposed
|
||||||
|
- [ ] [weak self] in closures
|
||||||
|
- [ ] Correct schedulers
|
||||||
|
- [ ] Error handling present
|
||||||
|
- [ ] Using BehaviorRelay
|
||||||
|
|
||||||
|
### Architecture 🏗️
|
||||||
|
- [ ] ViewModel → UseCase → Repository
|
||||||
|
- [ ] No business logic in ViewModel
|
||||||
|
- [ ] Dependencies injected
|
||||||
|
- [ ] BaseViewModel extended
|
||||||
|
- [ ] Repository pattern used
|
||||||
|
|
||||||
|
### Security 🔒
|
||||||
|
- [ ] Payment data in Keychain
|
||||||
|
- [ ] No sensitive logs
|
||||||
|
- [ ] HTTPS with pinning
|
||||||
|
- [ ] Input validation
|
||||||
|
|
||||||
|
### UI/UX 🎨
|
||||||
|
- [ ] title for simple titles
|
||||||
|
- [ ] titleView only with subtitle
|
||||||
|
- [ ] Accessibility configured
|
||||||
|
- [ ] Loading states shown
|
||||||
|
|
||||||
|
### Performance ⚡
|
||||||
|
- [ ] DB on background thread
|
||||||
|
- [ ] No retain cycles
|
||||||
|
- [ ] Image caching
|
||||||
|
- [ ] Memory management proper
|
||||||
|
```
|
||||||
185
skills/ios-naming-conventions/SKILL.md
Normal file
185
skills/ios-naming-conventions/SKILL.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
---
|
||||||
|
name: ios-naming-conventions
|
||||||
|
description: Check Swift naming conventions for iOS code. Validates class names, variables, functions, and IBOutlets against project standards. Use when reviewing code readability, checking abbreviations, or enforcing naming consistency in Swift files.
|
||||||
|
allowed-tools: Read, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# iOS Naming Conventions Checker
|
||||||
|
|
||||||
|
Validate Swift code naming against Payoo Merchant project standards for clarity and consistency.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- "check naming", "naming conventions", "code readability"
|
||||||
|
- "abbreviations", "variable names", "rename"
|
||||||
|
- Reviewing code quality or consistency
|
||||||
|
- Onboarding new developers
|
||||||
|
|
||||||
|
## Naming Rules Summary
|
||||||
|
|
||||||
|
### Types (Classes, Structs, Enums, Protocols)
|
||||||
|
- **PascalCase**: `PaymentViewModel`, `TransactionRepository`
|
||||||
|
- **Descriptive**: Purpose immediately clear
|
||||||
|
- **Proper suffixes**: ViewModel, ViewController, UseCase, Repository
|
||||||
|
|
||||||
|
### Variables & Properties
|
||||||
|
- **camelCase**: `paymentAmount`, `isProcessing`
|
||||||
|
- **Meaningful**: No abbreviations (except URL, ID, VC, UC)
|
||||||
|
- **Booleans**: Prefix `is`, `has`, `should`, `can`
|
||||||
|
- **Collections**: Plural names (`transactions`, `stores`)
|
||||||
|
|
||||||
|
### Functions & Methods
|
||||||
|
- **camelCase** with verb prefix
|
||||||
|
- **Actions**: `loadTransactions()`, `processPayment()`
|
||||||
|
- **Queries**: `getTransaction()`, `hasPermission()`
|
||||||
|
|
||||||
|
### IBOutlets
|
||||||
|
- **Type suffix**: `amountTextField`, `confirmButton`, `tableView`
|
||||||
|
|
||||||
|
## Review Process
|
||||||
|
|
||||||
|
### Step 1: Scan Code
|
||||||
|
|
||||||
|
Read files and identify all declarations:
|
||||||
|
- Class/struct/enum/protocol declarations
|
||||||
|
- Variable and property declarations
|
||||||
|
- Function declarations
|
||||||
|
- IBOutlet declarations
|
||||||
|
|
||||||
|
### Step 2: Check Against Rules
|
||||||
|
|
||||||
|
For each identifier, verify:
|
||||||
|
|
||||||
|
**Classes/Types**:
|
||||||
|
- ✅ PascalCase
|
||||||
|
- ✅ Descriptive (not generic like "Manager")
|
||||||
|
- ✅ No abbreviations (except standard ones)
|
||||||
|
- ✅ Proper suffix (ViewModel, UseCase, etc.)
|
||||||
|
|
||||||
|
**Variables**:
|
||||||
|
- ✅ camelCase
|
||||||
|
- ✅ Meaningful names
|
||||||
|
- ✅ Boolean prefixes (is/has/should/can)
|
||||||
|
- ✅ Plural for collections
|
||||||
|
- ✅ No single letters (except loop indices)
|
||||||
|
|
||||||
|
**Functions**:
|
||||||
|
- ✅ Verb-based names
|
||||||
|
- ✅ Clear action or query intent
|
||||||
|
- ✅ No generic names (doSomething, handle)
|
||||||
|
|
||||||
|
**IBOutlets**:
|
||||||
|
- ✅ Type suffix included
|
||||||
|
|
||||||
|
### Step 3: Generate Report
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Naming Conventions Review
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- 🔴 Critical (meaningless): X
|
||||||
|
- 🟠 High (abbreviations): X
|
||||||
|
- 🟡 Medium (missing prefixes): X
|
||||||
|
- 🟢 Low (style): X
|
||||||
|
|
||||||
|
## Issues by Type
|
||||||
|
|
||||||
|
### Classes/Structs/Enums
|
||||||
|
**File**: `path/to/file.swift:line`
|
||||||
|
Current: `PayVC`
|
||||||
|
Should be: `PaymentViewController`
|
||||||
|
Reason: [Explanation]
|
||||||
|
|
||||||
|
### Variables/Properties
|
||||||
|
[List with specific fixes]
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
[List with specific fixes]
|
||||||
|
|
||||||
|
### IBOutlets
|
||||||
|
[List with specific fixes]
|
||||||
|
|
||||||
|
## Batch Rename Suggestions
|
||||||
|
Found `amt` in 5 locations → Rename all to `paymentAmount`
|
||||||
|
|
||||||
|
## Good Examples Found ✅
|
||||||
|
[Acknowledge well-named elements]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Violations
|
||||||
|
|
||||||
|
### ❌ Abbreviations
|
||||||
|
```swift
|
||||||
|
class PayVC { } → PaymentViewController
|
||||||
|
let amt: Double → paymentAmount
|
||||||
|
func procPmt() { } → processPayment()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Single Letters
|
||||||
|
```swift
|
||||||
|
let x = transaction → currentTransaction
|
||||||
|
let a = amount → paymentAmount
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Generic/Meaningless
|
||||||
|
```swift
|
||||||
|
class Manager { } → PaymentManager
|
||||||
|
func doSomething() { } → processRefundRequest()
|
||||||
|
func handle() { } → handlePaymentError()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Missing Prefixes
|
||||||
|
```swift
|
||||||
|
let loading: Bool → isLoading: Bool
|
||||||
|
let valid: Bool → isValid: Bool
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Missing Type Suffix
|
||||||
|
```swift
|
||||||
|
@IBOutlet weak var amount: UITextField! → amountTextField
|
||||||
|
@IBOutlet weak var btn: UIButton! → confirmButton
|
||||||
|
```
|
||||||
|
|
||||||
|
## Search Patterns
|
||||||
|
|
||||||
|
Use Grep to find:
|
||||||
|
- **Abbreviations**: `(let|var)\s+[a-z]{1,3}\s*[=:]`
|
||||||
|
- **IBOutlets**: `@IBOutlet.*weak var`
|
||||||
|
- **Booleans**: `(let|var)\s+[a-z]+.*:\s*Bool`
|
||||||
|
|
||||||
|
## Output Guidelines
|
||||||
|
|
||||||
|
**For each violation**:
|
||||||
|
1. File path and line number
|
||||||
|
2. Current name
|
||||||
|
3. Recommended name
|
||||||
|
4. Reason for change
|
||||||
|
5. Impact on code clarity
|
||||||
|
|
||||||
|
**Prioritize**:
|
||||||
|
- Critical: Meaningless names (hurts maintainability)
|
||||||
|
- High: Abbreviations (reduces clarity)
|
||||||
|
- Medium: Missing prefixes/suffixes
|
||||||
|
- Low: Style inconsistencies
|
||||||
|
|
||||||
|
## Quick Fixes
|
||||||
|
|
||||||
|
1. **Expand Abbreviation**: Use Xcode refactor tool
|
||||||
|
2. **Add Boolean Prefix**: Rename with is/has/should/can
|
||||||
|
3. **Add Type Suffix**: Update IBOutlet names
|
||||||
|
|
||||||
|
## Common Abbreviations to Fix
|
||||||
|
|
||||||
|
| ❌ Bad | ✅ Good |
|
||||||
|
|--------|---------|
|
||||||
|
| amt | paymentAmount |
|
||||||
|
| trx, tx | transaction |
|
||||||
|
| btn | button |
|
||||||
|
| lbl | label |
|
||||||
|
| vc | viewController |
|
||||||
|
| uc | useCase |
|
||||||
|
| repo | repository |
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
**Detailed Examples**: See `examples.md` for extensive naming patterns and scenarios.
|
||||||
422
skills/ios-naming-conventions/examples.md
Normal file
422
skills/ios-naming-conventions/examples.md
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
# iOS Naming Conventions Examples
|
||||||
|
|
||||||
|
Comprehensive examples of good and bad naming patterns in Swift.
|
||||||
|
|
||||||
|
## Classes and Types
|
||||||
|
|
||||||
|
### ✅ Good Examples
|
||||||
|
```swift
|
||||||
|
// View Controllers
|
||||||
|
class PaymentViewController: UIViewController { }
|
||||||
|
class TransactionListViewController: UIViewController { }
|
||||||
|
class RefundConfirmationViewController: UIViewController { }
|
||||||
|
|
||||||
|
// ViewModels
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> { }
|
||||||
|
class StoresViewModel: BaseViewModel<StoresState> { }
|
||||||
|
class TransactionHistoryViewModel: BaseViewModel<TransactionHistoryState> { }
|
||||||
|
|
||||||
|
// Use Cases
|
||||||
|
protocol PaymentUseCase { }
|
||||||
|
class PaymentUseCaseImpl: PaymentUseCase { }
|
||||||
|
protocol RefundRequestUseCase { }
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
protocol PaymentRepository { }
|
||||||
|
class PaymentRepositoryImpl: PaymentRepository { }
|
||||||
|
|
||||||
|
// Models
|
||||||
|
struct Transaction { }
|
||||||
|
struct PaymentRequest { }
|
||||||
|
struct RefundRequest { }
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
enum PaymentMethod { }
|
||||||
|
enum TransactionStatus { }
|
||||||
|
enum RefundRequestError: Error { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Bad Examples
|
||||||
|
```swift
|
||||||
|
// Too abbreviated
|
||||||
|
class PayVC: UIViewController { } // → PaymentViewController
|
||||||
|
class RefReqVM { } // → RefundRequestViewModel
|
||||||
|
class TrxRepo { } // → TransactionRepository
|
||||||
|
|
||||||
|
// Too generic
|
||||||
|
class Manager { } // → PaymentManager or specific purpose
|
||||||
|
class Helper { } // → ValidationHelper or specific purpose
|
||||||
|
class Util { } // → DateFormatter or specific utility
|
||||||
|
|
||||||
|
// Missing suffixes
|
||||||
|
class Payment { } // → PaymentViewModel or PaymentUseCase
|
||||||
|
class Transaction { } // If it's a ViewModel → TransactionViewModel
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variables and Properties
|
||||||
|
|
||||||
|
### ✅ Good Examples
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel {
|
||||||
|
// State properties - descriptive
|
||||||
|
let paymentAmount = BehaviorRelay<String>(value: "")
|
||||||
|
let selectedPaymentMethod = BehaviorRelay<PaymentMethod?>(value: nil)
|
||||||
|
let transactionResult = BehaviorRelay<TransactionResult?>(value: nil)
|
||||||
|
|
||||||
|
// Boolean properties - with prefixes
|
||||||
|
let isProcessingPayment = BehaviorRelay<Bool>(value: false)
|
||||||
|
let hasNetworkConnection = BehaviorRelay<Bool>(value: true)
|
||||||
|
let shouldShowError = BehaviorRelay<Bool>(value: false)
|
||||||
|
let canSubmitPayment = BehaviorRelay<Bool>(value: false)
|
||||||
|
|
||||||
|
// Collections - plural
|
||||||
|
let transactions = BehaviorRelay<[Transaction]>(value: [])
|
||||||
|
let errorMessages = BehaviorRelay<[String]>(value: [])
|
||||||
|
let availablePaymentMethods = BehaviorRelay<[PaymentMethod]>(value: [])
|
||||||
|
|
||||||
|
// Dependencies - full names
|
||||||
|
private let paymentUseCase: PaymentUseCase
|
||||||
|
private let validationService: ValidationService
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Bad Examples
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel {
|
||||||
|
// Abbreviated
|
||||||
|
let amt = BehaviorRelay<String>(value: "") // → paymentAmount
|
||||||
|
let pmtMethod = BehaviorRelay<PaymentMethod?>(value: nil) // → paymentMethod
|
||||||
|
let trxResult = BehaviorRelay<TransactionResult?>(value: nil) // → transactionResult
|
||||||
|
|
||||||
|
// Generic/meaningless
|
||||||
|
let flag = BehaviorRelay<Bool>(value: false) // → isProcessing
|
||||||
|
let data = BehaviorRelay<[Any]>(value: []) // → transactions
|
||||||
|
let temp = BehaviorRelay<String>(value: "") // → What is this?
|
||||||
|
|
||||||
|
// Boolean without prefix
|
||||||
|
let loading: Bool // → isLoading
|
||||||
|
let valid: Bool // → isValid
|
||||||
|
let enabled: Bool // → isEnabled
|
||||||
|
|
||||||
|
// Single letter
|
||||||
|
let x = 0 // → transactionCount
|
||||||
|
let a = amount // → paymentAmount
|
||||||
|
|
||||||
|
// Inconsistent abbreviations
|
||||||
|
let paymentUC: PaymentUseCase // → paymentUseCase
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Functions and Methods
|
||||||
|
|
||||||
|
### ✅ Good Examples
|
||||||
|
```swift
|
||||||
|
class TransactionViewModel {
|
||||||
|
// Actions - verb-based, descriptive
|
||||||
|
func loadTransactions() { }
|
||||||
|
func refreshTransactionList() { }
|
||||||
|
func filterTransactionsByDate(from startDate: Date, to endDate: Date) { }
|
||||||
|
func processRefundRequest(for transactionId: String) { }
|
||||||
|
func validatePaymentAmount(_ amount: String) -> Bool { }
|
||||||
|
|
||||||
|
// Queries - return information
|
||||||
|
func getTransaction(by id: String) -> Transaction? { }
|
||||||
|
func calculateTotalAmount(for transactions: [Transaction]) -> Double { }
|
||||||
|
func hasUnprocessedTransactions() -> Bool { }
|
||||||
|
func isValidPaymentAmount(_ amount: String) -> Bool { }
|
||||||
|
|
||||||
|
// State handlers - clear purpose
|
||||||
|
func handlePaymentSuccess(_ result: PaymentResult) { }
|
||||||
|
func handlePaymentError(_ error: Error) { }
|
||||||
|
func handleNetworkConnectionLost() { }
|
||||||
|
|
||||||
|
// Setup/Configuration - clear intent
|
||||||
|
func setupUI() { }
|
||||||
|
func configureTableView() { }
|
||||||
|
func bindViewModel() { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Bad Examples
|
||||||
|
```swift
|
||||||
|
class TransactionViewModel {
|
||||||
|
// Too vague
|
||||||
|
func doSomething() { } // → processRefundRequest()
|
||||||
|
func process() { } // Process what? → processPayment()
|
||||||
|
func get() { } // Get what? → getTransactions()
|
||||||
|
func handle() { } // Handle what? → handleError()
|
||||||
|
func go() { } // Go where? → navigateToDetails()
|
||||||
|
|
||||||
|
// Noun-based instead of verb-based
|
||||||
|
func transaction() { } // → loadTransaction() or getTransaction()
|
||||||
|
func payment() { } // → processPayment()
|
||||||
|
|
||||||
|
// Abbreviated
|
||||||
|
func procPmt() { } // → processPayment()
|
||||||
|
func getTrx() { } // → getTransaction()
|
||||||
|
func valAmt(_ amt: String) { } // → validateAmount(_ amount: String)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IBOutlets
|
||||||
|
|
||||||
|
### ✅ Good Examples
|
||||||
|
```swift
|
||||||
|
class PaymentViewController: UIViewController {
|
||||||
|
// Text fields - with TextField suffix
|
||||||
|
@IBOutlet weak var paymentAmountTextField: UITextField!
|
||||||
|
@IBOutlet weak var merchantCodeTextField: UITextField!
|
||||||
|
@IBOutlet weak var notesTextField: UITextField!
|
||||||
|
|
||||||
|
// Labels - with Label suffix
|
||||||
|
@IBOutlet weak var currencyLabel: UILabel!
|
||||||
|
@IBOutlet weak var totalAmountLabel: UILabel!
|
||||||
|
@IBOutlet weak var errorMessageLabel: UILabel!
|
||||||
|
|
||||||
|
// Buttons - with Button suffix
|
||||||
|
@IBOutlet weak var confirmPaymentButton: UIButton!
|
||||||
|
@IBOutlet weak var cancelButton: UIButton!
|
||||||
|
@IBOutlet weak var submitButton: UIButton!
|
||||||
|
|
||||||
|
// Tables and collections - with TableView/CollectionView suffix
|
||||||
|
@IBOutlet weak var transactionTableView: UITableView!
|
||||||
|
@IBOutlet weak var storesCollectionView: UICollectionView!
|
||||||
|
|
||||||
|
// Other views - with type suffix
|
||||||
|
@IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
|
||||||
|
@IBOutlet weak var headerView: UIView!
|
||||||
|
@IBOutlet weak var footerContainerView: UIView!
|
||||||
|
@IBOutlet weak var paymentMethodSegmentedControl: UISegmentedControl!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Bad Examples
|
||||||
|
```swift
|
||||||
|
class PaymentViewController: UIViewController {
|
||||||
|
// Missing type suffix
|
||||||
|
@IBOutlet weak var amount: UITextField! // → amountTextField
|
||||||
|
@IBOutlet weak var currency: UILabel! // → currencyLabel
|
||||||
|
@IBOutlet weak var confirm: UIButton! // → confirmButton
|
||||||
|
|
||||||
|
// Abbreviated
|
||||||
|
@IBOutlet weak var lbl: UILabel! // → titleLabel
|
||||||
|
@IBOutlet weak var btn: UIButton! // → submitButton
|
||||||
|
@IBOutlet weak var txtField: UITextField! // → amountTextField
|
||||||
|
|
||||||
|
// Too generic
|
||||||
|
@IBOutlet weak var table: UITableView! // → transactionTableView
|
||||||
|
@IBOutlet weak var view: UIView! // → headerView
|
||||||
|
@IBOutlet weak var loading: UIActivityIndicatorView! // → loadingIndicator
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Naming
|
||||||
|
|
||||||
|
### ✅ Good Examples
|
||||||
|
```swift
|
||||||
|
class PaymentViewModelTests: XCTestCase {
|
||||||
|
// Format: test[MethodName]_[Scenario]_[ExpectedResult]
|
||||||
|
|
||||||
|
func testProcessPayment_WithValidAmount_CompletesSuccessfully() { }
|
||||||
|
func testProcessPayment_WithEmptyAmount_ShowsValidationError() { }
|
||||||
|
func testProcessPayment_WithNetworkError_ShowsErrorState() { }
|
||||||
|
func testLoadTransactions_WithCachedData_ReturnsDataImmediately() { }
|
||||||
|
func testRefundRequest_WhenAmountExceedsTransaction_Fails() { }
|
||||||
|
|
||||||
|
func testValidateAmount_BelowMinimum_ReturnsFalse() { }
|
||||||
|
func testValidateAmount_AboveMaximum_ReturnsFalse() { }
|
||||||
|
func testValidateAmount_ValidRange_ReturnsTrue() { }
|
||||||
|
|
||||||
|
func testSubmitRefund_WithValidData_UpdatesStateToSuccess() { }
|
||||||
|
func testSubmitRefund_WithInvalidData_UpdatesStateToError() { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Bad Examples
|
||||||
|
```swift
|
||||||
|
class PaymentViewModelTests: XCTestCase {
|
||||||
|
func test1() { } // Meaningless number
|
||||||
|
func testPayment() { } // Too vague
|
||||||
|
func testError() { } // What error scenario?
|
||||||
|
func testStuff() { } // Meaningless
|
||||||
|
func test_payment_works() { } // snake_case (wrong)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Class Examples
|
||||||
|
|
||||||
|
### ✅ Well-Named Class
|
||||||
|
```swift
|
||||||
|
class PaymentProcessingViewModel: BaseViewModel<PaymentState> {
|
||||||
|
// Dependencies - full, descriptive names
|
||||||
|
private let paymentUseCase: PaymentUseCase
|
||||||
|
private let validationService: ValidationService
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
// Input properties - clear, descriptive
|
||||||
|
let paymentAmount = BehaviorRelay<String>(value: "")
|
||||||
|
let selectedMerchantId = BehaviorRelay<String?>(value: nil)
|
||||||
|
let additionalNotes = BehaviorRelay<String>(value: "")
|
||||||
|
|
||||||
|
// State properties - boolean with prefixes
|
||||||
|
let isProcessingPayment = BehaviorRelay<Bool>(value: false)
|
||||||
|
let hasValidPaymentAmount = BehaviorRelay<Bool>(value: false)
|
||||||
|
let shouldEnableSubmitButton = BehaviorRelay<Bool>(value: false)
|
||||||
|
|
||||||
|
// Output properties - descriptive
|
||||||
|
let paymentResult = BehaviorRelay<PaymentResult?>(value: nil)
|
||||||
|
let errorMessage = BehaviorRelay<String?>(value: nil)
|
||||||
|
|
||||||
|
// Initializer - parameter names match properties
|
||||||
|
init(paymentUseCase: PaymentUseCase, validationService: ValidationService) {
|
||||||
|
self.paymentUseCase = paymentUseCase
|
||||||
|
self.validationService = validationService
|
||||||
|
super.init()
|
||||||
|
setupValidation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods - verb-based, descriptive
|
||||||
|
func processPaymentRequest() { }
|
||||||
|
func validatePaymentAmount() -> Bool { }
|
||||||
|
func clearPaymentForm() { }
|
||||||
|
func handlePaymentSuccess(_ result: PaymentResult) { }
|
||||||
|
func handlePaymentError(_ error: Error) { }
|
||||||
|
|
||||||
|
private func setupValidation() { }
|
||||||
|
private func updateSubmitButtonState() { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Poorly Named Class
|
||||||
|
```swift
|
||||||
|
class PaymentVM: BaseViewModel<PaymentState> { // Abbreviated
|
||||||
|
// Abbreviated dependencies
|
||||||
|
private let pmtUC: PaymentUseCase // → paymentUseCase
|
||||||
|
private let valService: ValidationService // → validationService
|
||||||
|
private let bag = DisposeBag() // → disposeBag
|
||||||
|
|
||||||
|
// Abbreviated/unclear properties
|
||||||
|
let amt = BehaviorRelay<String>(value: "") // → paymentAmount
|
||||||
|
let mid = BehaviorRelay<String?>(value: nil) // → merchantId
|
||||||
|
let notes = BehaviorRelay<String>(value: "") // Could be clearer
|
||||||
|
|
||||||
|
// Boolean without prefix
|
||||||
|
let processing = BehaviorRelay<Bool>(value: false) // → isProcessing
|
||||||
|
let valid = BehaviorRelay<Bool>(value: false) // → hasValidAmount
|
||||||
|
|
||||||
|
// Generic names
|
||||||
|
let result = BehaviorRelay<PaymentResult?>(value: nil) // → paymentResult
|
||||||
|
let error = BehaviorRelay<String?>(value: nil) // → errorMessage
|
||||||
|
|
||||||
|
// Abbreviated initializer
|
||||||
|
init(uc: PaymentUseCase, val: ValidationService) { // Bad parameter names
|
||||||
|
self.pmtUC = uc
|
||||||
|
self.valService = val
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vague method names
|
||||||
|
func process() { } // → processPaymentRequest()
|
||||||
|
func validate() -> Bool { } // → validatePaymentAmount()
|
||||||
|
func clear() { } // → clearPaymentForm()
|
||||||
|
func handle(_ r: PaymentResult) { } // → handlePaymentSuccess()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Refactoring Examples
|
||||||
|
|
||||||
|
### Example: Refactoring Poor Names
|
||||||
|
|
||||||
|
#### Before (Bad)
|
||||||
|
```swift
|
||||||
|
class TrxVM: BaseViewModel<TrxState> {
|
||||||
|
let trxs = BehaviorRelay<[Trx]>(value: [])
|
||||||
|
let loading = BehaviorRelay<Bool>(value: false)
|
||||||
|
|
||||||
|
func getTrx(id: String) -> Trx? {
|
||||||
|
return trxs.value.first { $0.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
func proc() {
|
||||||
|
loading.accept(true)
|
||||||
|
// Process transactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After (Good)
|
||||||
|
```swift
|
||||||
|
class TransactionViewModel: BaseViewModel<TransactionState> {
|
||||||
|
let transactions = BehaviorRelay<[Transaction]>(value: [])
|
||||||
|
let isLoading = BehaviorRelay<Bool>(value: false)
|
||||||
|
|
||||||
|
func getTransaction(by id: String) -> Transaction? {
|
||||||
|
return transactions.value.first { $0.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
func processTransactions() {
|
||||||
|
isLoading.accept(true)
|
||||||
|
// Process transactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changes Made**:
|
||||||
|
1. `TrxVM` → `TransactionViewModel` (full names, proper suffix)
|
||||||
|
2. `trxs` → `transactions` (no abbreviation, plural)
|
||||||
|
3. `loading` → `isLoading` (boolean prefix)
|
||||||
|
4. `getTrx` → `getTransaction` (full name, clear parameters)
|
||||||
|
5. `proc` → `processTransactions` (verb-based, descriptive)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference Checklist
|
||||||
|
|
||||||
|
Use this when reviewing naming:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Naming Conventions Checklist
|
||||||
|
|
||||||
|
### Classes/Structs/Enums
|
||||||
|
- [ ] PascalCase
|
||||||
|
- [ ] Descriptive and meaningful
|
||||||
|
- [ ] Proper suffix (ViewModel, UseCase, etc.)
|
||||||
|
- [ ] No abbreviations
|
||||||
|
|
||||||
|
### Variables/Properties
|
||||||
|
- [ ] camelCase
|
||||||
|
- [ ] Meaningful names
|
||||||
|
- [ ] Booleans have is/has/should/can prefix
|
||||||
|
- [ ] Collections are plural
|
||||||
|
- [ ] No single letters (except loops)
|
||||||
|
- [ ] No abbreviations
|
||||||
|
|
||||||
|
### Functions/Methods
|
||||||
|
- [ ] camelCase
|
||||||
|
- [ ] Verb-based (actions) or get/has/is (queries)
|
||||||
|
- [ ] Descriptive of purpose
|
||||||
|
- [ ] Clear parameter names
|
||||||
|
|
||||||
|
### IBOutlets
|
||||||
|
- [ ] Include type suffix
|
||||||
|
- [ ] Descriptive of purpose
|
||||||
|
- [ ] camelCase
|
||||||
|
|
||||||
|
### General
|
||||||
|
- [ ] No generic names (Manager, Helper, Util)
|
||||||
|
- [ ] Consistent naming style
|
||||||
|
- [ ] Easy to understand without context
|
||||||
|
```
|
||||||
605
skills/mcp-tool-generator/SKILL.md
Normal file
605
skills/mcp-tool-generator/SKILL.md
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
---
|
||||||
|
name: mcp-tool-generator
|
||||||
|
description: Generate new MCP tools for GitLab operations following the project's standardized pattern. Creates complete TypeScript files with imports, registration functions, Zod schemas, error handling, and format options. Supports simple CRUD operations, complex multi-action tools, and advanced patterns like discussion management. Use when "create mcp tool", "generate gitlab tool", "new tool for", "add tool to gitlab", or building new GitLab integration features.
|
||||||
|
tools: [Read, Write, Glob, Grep]
|
||||||
|
---
|
||||||
|
|
||||||
|
# MCP Tool Generator
|
||||||
|
|
||||||
|
Generate new MCP tools following the standardized patterns from the project. Creates complete tool files with proper imports, Zod schemas, error handling, and GitLab API integration.
|
||||||
|
|
||||||
|
## Activation Triggers
|
||||||
|
|
||||||
|
- "create an mcp tool for..."
|
||||||
|
- "generate a gitlab tool to..."
|
||||||
|
- "I need a new tool that..."
|
||||||
|
- "add a tool for [operation]"
|
||||||
|
- "create tool to [action] [resource]"
|
||||||
|
|
||||||
|
## Tool Types Supported
|
||||||
|
|
||||||
|
### 1. Simple CRUD Tools
|
||||||
|
Basic get/list/create/update/delete operations with standard patterns:
|
||||||
|
- Get single resource (issue, MR, milestone, etc.)
|
||||||
|
- List multiple resources with filtering and pagination
|
||||||
|
- Create new resources
|
||||||
|
- Update existing resources
|
||||||
|
- Delete resources
|
||||||
|
|
||||||
|
**Pattern**: `gitlab-[action]-[resource]` (e.g., `gitlab-get-issue`, `gitlab-list-pipelines`)
|
||||||
|
|
||||||
|
### 2. Multi-Action Tools
|
||||||
|
Comprehensive tools that handle multiple related operations in one tool:
|
||||||
|
- Multiple actions via `action` enum parameter
|
||||||
|
- Conditional logic based on action type
|
||||||
|
- Structured responses with status/action/message format
|
||||||
|
- More efficient than multiple separate tools
|
||||||
|
|
||||||
|
**Pattern**: `gitlab-[resource]-[operation]` (e.g., `gitlab-manage-issue`)
|
||||||
|
|
||||||
|
### 3. Complex Operation Tools
|
||||||
|
Tools with advanced logic:
|
||||||
|
- Discussion/comment management with update detection
|
||||||
|
- Multi-step workflows
|
||||||
|
- Direct API calls using fetch for specific needs
|
||||||
|
- Position-based operations (code reviews, inline comments)
|
||||||
|
|
||||||
|
**Pattern**: Based on specific operation (e.g., `gitlab-review-merge-request-code`)
|
||||||
|
|
||||||
|
## Autonomous Generation Process
|
||||||
|
|
||||||
|
### Step 1: Analyze User Request
|
||||||
|
|
||||||
|
Extract key information:
|
||||||
|
1. **Tool Type**: Simple CRUD, multi-action, or complex?
|
||||||
|
2. **Tool Purpose**: What GitLab operation? (e.g., "get merge request details", "manage issues", "review code")
|
||||||
|
3. **Resource Type**: What GitLab entity? (issue, MR, branch, milestone, pipeline, label, etc.)
|
||||||
|
4. **Action Type**: What operation? (get, list, create, update, delete, search, manage, review, etc.)
|
||||||
|
5. **Required Parameters**: What inputs needed? (projectname, IID, branch name, action, etc.)
|
||||||
|
6. **Optional Parameters**: What's optional? (format, labels, assignee, filters, etc.)
|
||||||
|
7. **Special Features**: Multi-action? Position-based? Discussion management?
|
||||||
|
|
||||||
|
### Step 2: Auto-Generate Names
|
||||||
|
|
||||||
|
**Tool Name** (kebab-case):
|
||||||
|
- Simple CRUD: `gitlab-[action]-[resource]`
|
||||||
|
- Examples: `gitlab-get-merge-request`, `gitlab-list-pipelines`, `gitlab-create-branch`
|
||||||
|
- Multi-action: `gitlab-[manage|handle]-[resource]`
|
||||||
|
- Examples: `gitlab-manage-issue`, `gitlab-handle-milestone`
|
||||||
|
- Complex: `gitlab-[specific-operation]`
|
||||||
|
- Examples: `gitlab-review-merge-request-code`, `gitlab-find-related-issues`
|
||||||
|
|
||||||
|
**Function Name** (PascalCase):
|
||||||
|
- Pattern: `register[Action][Resource]`
|
||||||
|
- Examples:
|
||||||
|
- `gitlab-get-merge-request` → `registerGetMergeRequest`
|
||||||
|
- `gitlab-manage-issue` → `registerManageIssue`
|
||||||
|
- `gitlab-review-merge-request-code` → `registerReviewMergeRequestCode`
|
||||||
|
|
||||||
|
**File Name** (kebab-case):
|
||||||
|
- Pattern: `gitlab-[tool-name].ts`
|
||||||
|
- Location: `src/tools/gitlab/`
|
||||||
|
|
||||||
|
### Step 3: Select Tool Pattern
|
||||||
|
|
||||||
|
#### Pattern A: Simple CRUD Tool
|
||||||
|
|
||||||
|
**Use when**: Single operation (get, list, create, update, delete)
|
||||||
|
|
||||||
|
**Standard features**:
|
||||||
|
- `projectname` parameter (optional, with prompt fallback)
|
||||||
|
- `format` parameter (detailed/concise) for get/list operations
|
||||||
|
- HTML content cleaning with `cleanGitLabHtmlContent()`
|
||||||
|
- Project validation before API calls
|
||||||
|
- Descriptive error messages
|
||||||
|
- Emojis in concise format
|
||||||
|
|
||||||
|
**Template structure**:
|
||||||
|
```typescript
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { cleanGitLabHtmlContent } from '../../core/utils';
|
||||||
|
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
|
||||||
|
|
||||||
|
export function register{FunctionName}(server: McpServer) {
|
||||||
|
server.registerTool(
|
||||||
|
"{tool-name}",
|
||||||
|
{
|
||||||
|
title: "{Human Readable Title}",
|
||||||
|
description: "{Detailed description}",
|
||||||
|
inputSchema: {
|
||||||
|
{param1}: z.{type}().describe("{description}"),
|
||||||
|
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
|
||||||
|
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async ({ {params}, projectname, format = "detailed" }) => {
|
||||||
|
try {
|
||||||
|
// Standard workflow
|
||||||
|
} catch (e) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: String(e) }) }] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pattern B: Multi-Action Tool
|
||||||
|
|
||||||
|
**Use when**: Multiple related operations on same resource type
|
||||||
|
|
||||||
|
**Standard features**:
|
||||||
|
- `action` parameter with enum of actions
|
||||||
|
- Switch/case logic for each action
|
||||||
|
- Structured responses: `{ status: "success"/"failure", action: "...", message: "...", [resource]: {...} }`
|
||||||
|
- Direct API calls using fetch when needed
|
||||||
|
- Conditional parameters based on action
|
||||||
|
- No format parameter (uses structured JSON)
|
||||||
|
|
||||||
|
**Template structure**:
|
||||||
|
```typescript
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { cleanGitLabHtmlContent } from '../../core/utils';
|
||||||
|
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
|
||||||
|
|
||||||
|
export function register{FunctionName}(server: McpServer) {
|
||||||
|
server.registerTool(
|
||||||
|
"{tool-name}",
|
||||||
|
{
|
||||||
|
title: "{Human Readable Title}",
|
||||||
|
description: "{Comprehensive description covering all actions}",
|
||||||
|
inputSchema: {
|
||||||
|
{resourceId}: z.number().describe("The ID/IID of the resource"),
|
||||||
|
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
|
||||||
|
action: z.enum(["action1", "action2", "action3"]).describe("Action to perform"),
|
||||||
|
// Conditional parameters for different actions
|
||||||
|
param1: z.{type}().optional().describe("For action1: description"),
|
||||||
|
param2: z.{type}().optional().describe("For action2: description")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async ({ {resourceId}, projectname, action, {params} }) => {
|
||||||
|
try {
|
||||||
|
// Get project and resource
|
||||||
|
const projectName = projectname || await getProjectNameFromUser(server, false, "prompt");
|
||||||
|
if (!projectName) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found" }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = await getGitLabService(server);
|
||||||
|
const projectId = await service.getProjectId(projectName);
|
||||||
|
if (!projectId) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Project "${projectName}" not found` }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get resource first
|
||||||
|
const rawResource = await service.get{Resource}(projectId, {resourceId});
|
||||||
|
if (!rawResource) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Resource not found` }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const resource = cleanGitLabHtmlContent(rawResource, ['description', 'title']);
|
||||||
|
|
||||||
|
// Handle actions
|
||||||
|
switch (action) {
|
||||||
|
case "action1":
|
||||||
|
// Implementation
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'success',
|
||||||
|
action: 'action1',
|
||||||
|
message: 'Action completed',
|
||||||
|
{resource}: { /* key fields */ }
|
||||||
|
}, null, 2) }] };
|
||||||
|
|
||||||
|
case "action2":
|
||||||
|
// Implementation
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
error: `Unknown action "${action}"`
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
error: String(e)
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pattern C: Complex Operation Tool
|
||||||
|
|
||||||
|
**Use when**: Advanced logic like discussion management, position-based operations, multi-step workflows
|
||||||
|
|
||||||
|
**Standard features**:
|
||||||
|
- Specialized parameters (may not include projectname if using projectId directly)
|
||||||
|
- Custom logic for specific use cases
|
||||||
|
- May use direct API calls
|
||||||
|
- May fetch and update existing data
|
||||||
|
- Structured responses appropriate to operation
|
||||||
|
|
||||||
|
**Template structure**: Highly variable based on specific needs
|
||||||
|
|
||||||
|
### Step 4: Generate Zod Schema
|
||||||
|
|
||||||
|
**Common Parameter Patterns**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// IDs (internal issue/MR number)
|
||||||
|
{name}Iid: z.number().describe("The internal ID (IID) of the {resource} to {action}")
|
||||||
|
|
||||||
|
// Project ID (for tools that need direct ID)
|
||||||
|
projectId: z.number().describe("The project ID")
|
||||||
|
|
||||||
|
// Names/identifiers
|
||||||
|
{name}: z.string().describe("{Resource} name (e.g., 'feature/user-auth')")
|
||||||
|
|
||||||
|
// Action enums (for multi-action tools)
|
||||||
|
action: z.enum(["action1", "action2", "action3"]).describe("Action to perform on the {resource}")
|
||||||
|
|
||||||
|
// Optional filters
|
||||||
|
state: z.enum(["opened", "closed", "all"]).optional().describe("Filter by state (default: 'opened')")
|
||||||
|
labels: z.string().optional().describe("Comma-separated list of label names to filter by")
|
||||||
|
// OR for multi-action tools:
|
||||||
|
labels: z.array(z.string()).optional().describe("For add-labels action: labels to add")
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
page: z.number().optional().describe("Page number for pagination (default: 1)")
|
||||||
|
perPage: z.number().optional().describe("Number of items per page (default: 20, max: 100)")
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
dueDate: z.string().optional().describe("Due date in ISO 8601 format (YYYY-MM-DD)")
|
||||||
|
|
||||||
|
// Position-based parameters (for code review tools)
|
||||||
|
baseSha: z.string().describe("Base SHA for the diff")
|
||||||
|
startSha: z.string().describe("Start SHA for the diff")
|
||||||
|
headSha: z.string().describe("Head SHA for the diff")
|
||||||
|
newPath: z.string().describe("Path to the file being reviewed")
|
||||||
|
newLine: z.number().optional().describe("Line number in the new file")
|
||||||
|
|
||||||
|
// Project (standard for simple CRUD, optional for complex tools)
|
||||||
|
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)")
|
||||||
|
|
||||||
|
// Format (only for simple get/list operations)
|
||||||
|
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important notes**:
|
||||||
|
- Add `.describe()` with clear examples for all parameters
|
||||||
|
- Use `z.array(z.string())` for arrays in multi-action tools
|
||||||
|
- Note when square brackets `[]` are allowed in descriptions for paths/labels/markdown
|
||||||
|
- Make parameters optional when sensible defaults exist
|
||||||
|
|
||||||
|
### Step 5: Generate Response Formats
|
||||||
|
|
||||||
|
#### Simple CRUD Tools (Pattern A)
|
||||||
|
|
||||||
|
**Concise format** (with emojis):
|
||||||
|
```typescript
|
||||||
|
if (format === "concise") {
|
||||||
|
return { content: [{ type: "text", text:
|
||||||
|
`{emoji} {Resource} #{id}: {title}\n` +
|
||||||
|
`📊 Status: {state}\n` +
|
||||||
|
`👤 {role}: {user}\n` +
|
||||||
|
`🏷️ Labels: {labels}\n` +
|
||||||
|
`🎯 Milestone: {milestone}\n` +
|
||||||
|
`📅 Due: {due_date}\n` +
|
||||||
|
`🔗 URL: {web_url}`
|
||||||
|
}] };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detailed format** (full JSON):
|
||||||
|
```typescript
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({resource}, null, 2) }] };
|
||||||
|
```
|
||||||
|
|
||||||
|
**Emoji Guide**:
|
||||||
|
- 🔍 - Get/View operations
|
||||||
|
- 📋 - List operations
|
||||||
|
- ✨ - Create operations
|
||||||
|
- 🔄 - Update operations
|
||||||
|
- 🗑️ - Delete operations
|
||||||
|
- 📊 - Status/State
|
||||||
|
- 👤 - User/Assignee
|
||||||
|
- 🏷️ - Labels
|
||||||
|
- 🎯 - Milestone
|
||||||
|
- 📅 - Dates
|
||||||
|
- 🔗 - URLs
|
||||||
|
- ✅ - Success/Completed
|
||||||
|
- ❌ - Error/Failed
|
||||||
|
|
||||||
|
#### Multi-Action Tools (Pattern B)
|
||||||
|
|
||||||
|
**Structured JSON format**:
|
||||||
|
```typescript
|
||||||
|
// Success response
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
action: 'action-name',
|
||||||
|
message: 'Human-readable success message',
|
||||||
|
{resource}: {
|
||||||
|
id: resource.id,
|
||||||
|
iid: resource.iid,
|
||||||
|
title: resource.title,
|
||||||
|
webUrl: resource.web_url,
|
||||||
|
// Other key fields relevant to the action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failure response
|
||||||
|
{
|
||||||
|
status: 'failure',
|
||||||
|
action: 'action-name',
|
||||||
|
error: 'Detailed error message with context',
|
||||||
|
{resource}: {
|
||||||
|
id: resource.id,
|
||||||
|
iid: resource.iid,
|
||||||
|
title: resource.title,
|
||||||
|
webUrl: resource.web_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Complex Tools (Pattern C)
|
||||||
|
|
||||||
|
Custom format based on operation needs. Examples:
|
||||||
|
```typescript
|
||||||
|
// Discussion update/create
|
||||||
|
{
|
||||||
|
action: "updated" | "created",
|
||||||
|
discussion_id: "...",
|
||||||
|
note_id: "...",
|
||||||
|
updated_note: {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Add Error Handling
|
||||||
|
|
||||||
|
**Standard Error Patterns**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Project not selected (for tools with projectname parameter)
|
||||||
|
if (!projectName) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
error: "Project not found or not selected. Please provide a valid project name."
|
||||||
|
}) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project not found
|
||||||
|
if (!projectId) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.`
|
||||||
|
}) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource not found
|
||||||
|
if (!resource) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
error: `{Resource} not found. Please verify the {parameters} are correct.`
|
||||||
|
}) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing required parameters (for multi-action tools)
|
||||||
|
if (!requiredParam) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
action: action,
|
||||||
|
error: "Required parameter missing. Please specify...",
|
||||||
|
{resource}: { /* minimal info */ }
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// API call failure (for multi-action tools using fetch)
|
||||||
|
if (!response.ok) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
action: action,
|
||||||
|
error: `Failed to {action}. Status: ${response.status}`,
|
||||||
|
{resource}: { /* minimal info */ }
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// General error (catch block)
|
||||||
|
catch (e) {
|
||||||
|
// For simple CRUD tools:
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
error: `Error {operation}: ${String(e)}. Please check your GitLab connection and permissions.`
|
||||||
|
}) }] };
|
||||||
|
|
||||||
|
// For multi-action tools:
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
error: `Error {operation}: ${String(e)}`
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Register in gitlab-tool.ts
|
||||||
|
|
||||||
|
After creating the tool file, add registration:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In src/tools/gitlab-tool.ts
|
||||||
|
|
||||||
|
// Add import at top
|
||||||
|
import { register{FunctionName} } from './gitlab/gitlab-{tool-name}';
|
||||||
|
|
||||||
|
// Add registration in registerGitlabTools function
|
||||||
|
export function registerGitlabTools(server: McpServer) {
|
||||||
|
// ... other registrations
|
||||||
|
register{FunctionName}(server);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interactive Generation Workflow
|
||||||
|
|
||||||
|
### Ask User (Only if unclear):
|
||||||
|
|
||||||
|
1. **Tool Type**:
|
||||||
|
- "Is this a simple CRUD operation, multi-action tool, or complex operation?"
|
||||||
|
- Clarify if multiple actions should be combined in one tool
|
||||||
|
|
||||||
|
2. **Tool Purpose**:
|
||||||
|
- "What GitLab operation should this tool perform?"
|
||||||
|
- Examples: "Get merge request details", "Manage issues (get, update, close)", "Review code inline"
|
||||||
|
|
||||||
|
3. **Required Parameters**:
|
||||||
|
- "What parameters are required?"
|
||||||
|
- Examples: "merge request IID", "issue IID and action type", "project ID and position data"
|
||||||
|
|
||||||
|
4. **Optional Parameters**:
|
||||||
|
- "Any optional filters or options?"
|
||||||
|
- Examples: "state filter", "label filter", "format option"
|
||||||
|
|
||||||
|
5. **API Method** (if not obvious):
|
||||||
|
- "Which GitLab service method to use?"
|
||||||
|
- Check `src/services/gitlab-client.ts` for available methods
|
||||||
|
- Note if direct fetch API calls are needed
|
||||||
|
|
||||||
|
### Generate Files:
|
||||||
|
|
||||||
|
1. **Create tool file**: `src/tools/gitlab/gitlab-{tool-name}.ts`
|
||||||
|
2. **Show registration code** for `src/tools/gitlab-tool.ts`
|
||||||
|
3. **Provide usage examples** based on tool type
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
After generating the tool:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
✅ MCP Tool Created: {tool-name}
|
||||||
|
|
||||||
|
📁 Files Created:
|
||||||
|
- `src/tools/gitlab/gitlab-{tool-name}.ts`
|
||||||
|
|
||||||
|
🔧 Type: {Simple CRUD | Multi-Action | Complex Operation}
|
||||||
|
🔧 Function: register{FunctionName}
|
||||||
|
|
||||||
|
📝 Next Steps:
|
||||||
|
1. Add registration to `src/tools/gitlab-tool.ts`:
|
||||||
|
```typescript
|
||||||
|
import { register{FunctionName} } from './gitlab/gitlab-{tool-name}';
|
||||||
|
// In registerGitlabTools:
|
||||||
|
register{FunctionName}(server);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Rebuild the project:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Test the tool:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
🎯 Usage Examples:
|
||||||
|
{Type-specific examples}
|
||||||
|
|
||||||
|
📖 Tool registered as: "{tool-name}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitLab Service Methods Reference
|
||||||
|
|
||||||
|
Common methods available in `gitlab-client.ts`. Latest update 31/10/2025:
|
||||||
|
|
||||||
|
**Issues**:
|
||||||
|
- `getIssue(projectId, iid)`
|
||||||
|
- `getIssues(projectId, options)`
|
||||||
|
- `createIssue(projectId, data)`
|
||||||
|
- `updateIssue(projectId, iid, data)`
|
||||||
|
|
||||||
|
**Merge Requests**:
|
||||||
|
- `getMergeRequest(projectId, iid)`
|
||||||
|
- `getMergeRequests(projectId, options)`
|
||||||
|
- `createMergeRequest(projectId, data)`
|
||||||
|
- `updateMergeRequest(projectId, iid, data)`
|
||||||
|
- `approveMergeRequest(projectId, iid)`
|
||||||
|
- `getMrDiscussions(projectId, iid)`
|
||||||
|
- `addMrComments(projectId, iid, data)`
|
||||||
|
- `updateMrDiscussionNote(projectId, iid, discussionId, noteId, body)`
|
||||||
|
|
||||||
|
**Branches**:
|
||||||
|
- `getBranches(projectId, options)`
|
||||||
|
- `createBranch(projectId, branchName, ref)`
|
||||||
|
- `deleteBranch(projectId, branchName)`
|
||||||
|
|
||||||
|
**Pipelines**:
|
||||||
|
- `getPipelines(projectId, options)`
|
||||||
|
- `getPipeline(projectId, pipelineId)`
|
||||||
|
- `createPipeline(projectId, ref)`
|
||||||
|
|
||||||
|
**Milestones**:
|
||||||
|
- `getMilestone(projectId, milestoneId)`
|
||||||
|
- `getMilestones(projectId, options)`
|
||||||
|
- `createMilestone(projectId, data)`
|
||||||
|
- `updateMilestone(projectId, milestoneId, data)`
|
||||||
|
|
||||||
|
**Projects**:
|
||||||
|
- `getProjectId(projectName)`
|
||||||
|
- `getProject(projectId)`
|
||||||
|
- `searchProjects(search)`
|
||||||
|
|
||||||
|
**Users**:
|
||||||
|
- `getUserIdByUsername(username)`
|
||||||
|
|
||||||
|
**Direct Fetch API**:
|
||||||
|
For operations not covered by service methods, use direct fetch:
|
||||||
|
```typescript
|
||||||
|
const response = await fetch(`${service.gitlabUrl}/api/v4/projects/${projectId}/{endpoint}`, {
|
||||||
|
method: 'PUT' | 'POST' | 'GET' | 'DELETE',
|
||||||
|
headers: service['getHeaders'](),
|
||||||
|
body: JSON.stringify({...})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Patterns to Follow
|
||||||
|
|
||||||
|
1. **Always use appropriate tool pattern** based on operation type
|
||||||
|
2. **Simple CRUD tools**: Include `projectname` and `format` parameters
|
||||||
|
3. **Multi-action tools**: Use `action` enum and structured responses
|
||||||
|
4. **Always clean HTML content** with `cleanGitLabHtmlContent()` where applicable
|
||||||
|
5. **Always validate project exists** before API calls (if using projectname)
|
||||||
|
6. **Always use descriptive error messages** with context
|
||||||
|
7. **Always use emojis in concise format** for simple CRUD tools
|
||||||
|
8. **Always follow kebab-case** for file and tool names
|
||||||
|
9. **Always follow PascalCase** for function names
|
||||||
|
10. **Always provide detailed Zod descriptions** with examples
|
||||||
|
11. **Always handle null/undefined responses** gracefully
|
||||||
|
12. **Multi-action tools**: Return structured JSON with status/action/message
|
||||||
|
13. **Direct API calls**: Use fetch and check response.ok
|
||||||
|
14. **Note square bracket support**: Add notes about `[]` support in descriptions where relevant (file paths, labels, markdown)
|
||||||
|
|
||||||
|
## Quality Checklist
|
||||||
|
|
||||||
|
Before presenting the generated tool:
|
||||||
|
|
||||||
|
- ✅ File name is kebab-case
|
||||||
|
- ✅ Function name is PascalCase with "register" prefix
|
||||||
|
- ✅ All imports are correct
|
||||||
|
- ✅ Zod schema has detailed descriptions
|
||||||
|
- ✅ Appropriate tool pattern selected (Simple CRUD / Multi-Action / Complex)
|
||||||
|
- ✅ For simple CRUD: projectname optional, format parameter included
|
||||||
|
- ✅ For multi-action: action enum, structured responses, conditional params
|
||||||
|
- ✅ HTML content cleaned where applicable
|
||||||
|
- ✅ Error messages are descriptive and actionable
|
||||||
|
- ✅ Response format matches tool type
|
||||||
|
- ✅ Try-catch wraps the entire handler
|
||||||
|
- ✅ All responses follow `{ content: [{ type: "text", text: ... }] }` format
|
||||||
|
- ✅ Tool follows MCP SDK patterns
|
||||||
|
- ✅ Code matches project conventions from CLAUDE.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to generate MCP tools!** Tell me what GitLab operation you want to create a tool for.
|
||||||
556
skills/mcp-tool-generator/examples.md
Normal file
556
skills/mcp-tool-generator/examples.md
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
# MCP Tool Generator Examples
|
||||||
|
|
||||||
|
Complete examples of generated MCP tools following the standardized patterns. Includes simple CRUD tools, multi-action tools, and complex operation tools.
|
||||||
|
|
||||||
|
## Example 1: Simple CRUD - Get Merge Request Details
|
||||||
|
|
||||||
|
**Tool Type**: Simple CRUD (Pattern A)
|
||||||
|
|
||||||
|
### User Request
|
||||||
|
"Create a tool to get merge request details by IID"
|
||||||
|
|
||||||
|
### Generated File: `src/tools/gitlab/gitlab-get-merge-request.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { cleanGitLabHtmlContent } from '../../core/utils';
|
||||||
|
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
|
||||||
|
|
||||||
|
export function registerGetMergeRequest(server: McpServer) {
|
||||||
|
server.registerTool(
|
||||||
|
"gitlab-get-merge-request",
|
||||||
|
{
|
||||||
|
title: "Get Merge Request Details",
|
||||||
|
description: "Retrieve detailed information for a specific merge request by IID in a GitLab project. Returns merge request metadata including title, description, state, author, assignee, reviewers, labels, milestone, source/target branches, and approval status. Use this when you need comprehensive information about a specific merge request.",
|
||||||
|
inputSchema: {
|
||||||
|
mergeRequestIid: z.number().describe("The internal ID (IID) of the merge request to retrieve"),
|
||||||
|
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
|
||||||
|
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async ({ mergeRequestIid, projectname, format = "detailed" }) => {
|
||||||
|
const iid = mergeRequestIid as number;
|
||||||
|
try {
|
||||||
|
const projectName = projectname || await getProjectNameFromUser(server, false, "Please select the project for getting merge request");
|
||||||
|
if (!projectName) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found or not selected. Please provide a valid project name." }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = await getGitLabService(server);
|
||||||
|
const projectId = await service.getProjectId(projectName);
|
||||||
|
if (!projectId) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.` }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawMr = await service.getMergeRequest(projectId, iid);
|
||||||
|
if (!rawMr) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Merge request !${iid} not found in project "${projectName}". Please verify the merge request IID is correct.` }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean HTML content from merge request fields
|
||||||
|
const mr = cleanGitLabHtmlContent(rawMr, ['description', 'title']);
|
||||||
|
|
||||||
|
// Format response based on requested format
|
||||||
|
if (format === "concise") {
|
||||||
|
const conciseInfo = {
|
||||||
|
title: mr.title,
|
||||||
|
state: mr.state,
|
||||||
|
author: mr.author?.name || "Unknown",
|
||||||
|
assignee: mr.assignee?.name || "Unassigned",
|
||||||
|
labels: mr.labels || [],
|
||||||
|
milestone: mr.milestone?.title || "No milestone",
|
||||||
|
source_branch: mr.source_branch,
|
||||||
|
target_branch: mr.target_branch,
|
||||||
|
web_url: mr.web_url
|
||||||
|
};
|
||||||
|
return { content: [{ type: "text", text: `🔍 MR !${iid}: ${mr.title}\n📊 Status: ${mr.state}\n👤 Author: ${conciseInfo.author}\n👤 Assignee: ${conciseInfo.assignee}\n🏷️ Labels: ${conciseInfo.labels.join(', ') || 'None'}\n🎯 Milestone: ${conciseInfo.milestone}\n🔀 ${conciseInfo.source_branch} → ${conciseInfo.target_branch}\n🔗 URL: ${mr.web_url}` }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(mr, null, 2) }] };
|
||||||
|
} catch (e) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Error retrieving merge request !${iid}: ${String(e)}. Please check your GitLab connection and permissions.` }) }] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registration in `gitlab-tool.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { registerGetMergeRequest } from './gitlab/gitlab-get-merge-request';
|
||||||
|
|
||||||
|
export function registerGitlabTools(server: McpServer) {
|
||||||
|
// ... other registrations
|
||||||
|
registerGetMergeRequest(server);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 2: Multi-Action Tool - Manage Issues
|
||||||
|
|
||||||
|
**Tool Type**: Multi-Action (Pattern B)
|
||||||
|
|
||||||
|
### User Request
|
||||||
|
"Create a tool that can manage issues - get details, close, reopen, add labels, set assignees, and set due dates"
|
||||||
|
|
||||||
|
### Generated File: `src/tools/gitlab/gitlab-manage-issue.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { cleanGitLabHtmlContent } from '../../core/utils';
|
||||||
|
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
|
||||||
|
|
||||||
|
export function registerManageIssue(server: McpServer) {
|
||||||
|
server.registerTool(
|
||||||
|
"gitlab-manage-issue",
|
||||||
|
{
|
||||||
|
title: "Manage GitLab Issue",
|
||||||
|
description: "Comprehensive issue management tool that can get, update, or modify issues in a single operation. More efficient than using multiple separate tools. Supports getting issue details, updating status, adding labels, setting assignees, and modifying due dates.",
|
||||||
|
inputSchema: {
|
||||||
|
issueIid: z.number().describe("The internal ID (IID) of the issue to manage"),
|
||||||
|
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
|
||||||
|
action: z.enum(["get", "close", "reopen", "add-labels", "set-assignee", "set-due-date"]).describe("Action to perform on the issue"),
|
||||||
|
// Parameters for different actions
|
||||||
|
labels: z.array(z.string()).optional().describe("For add-labels action: labels to add to the issue. Square brackets [] are allowed in label names."),
|
||||||
|
assignee_username: z.string().optional().describe("For set-assignee action: username to assign the issue to"),
|
||||||
|
due_date: z.string().optional().describe("For set-due-date action: due date in YYYY-MM-DD format")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async ({ issueIid, projectname, action, labels, assignee_username, due_date }) => {
|
||||||
|
const iid = issueIid as number;
|
||||||
|
try {
|
||||||
|
const projectName = projectname || await getProjectNameFromUser(server, false, "Please select the project for issue management");
|
||||||
|
if (!projectName) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found or not selected. Please provide a valid project name." }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = await getGitLabService(server);
|
||||||
|
const projectId = await service.getProjectId(projectName);
|
||||||
|
if (!projectId) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.` }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get issue first for all actions
|
||||||
|
const rawIssue = await service.getIssue(projectId, iid);
|
||||||
|
if (!rawIssue) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Issue #${iid} not found in project "${projectName}". Please verify the issue IID is correct.` }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean HTML content from issue fields
|
||||||
|
const issue = cleanGitLabHtmlContent(rawIssue, ['description', 'title']);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "get":
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'success',
|
||||||
|
action: 'get',
|
||||||
|
issue: {
|
||||||
|
id: issue.id,
|
||||||
|
iid: issue.iid,
|
||||||
|
title: issue.title,
|
||||||
|
webUrl: issue.web_url,
|
||||||
|
state: issue.state,
|
||||||
|
assignee: issue.assignee?.name || null,
|
||||||
|
labels: issue.labels || [],
|
||||||
|
milestone: issue.milestone?.title || null,
|
||||||
|
dueDate: issue.due_date || null,
|
||||||
|
description: issue.description
|
||||||
|
}
|
||||||
|
}, null, 2) }] };
|
||||||
|
|
||||||
|
case "close":
|
||||||
|
const closeResponse = await fetch(`${service.gitlabUrl}/api/v4/projects/${projectId}/issues/${iid}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: service['getHeaders'](),
|
||||||
|
body: JSON.stringify({ state_event: "close" })
|
||||||
|
});
|
||||||
|
if (!closeResponse.ok) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
action: 'close',
|
||||||
|
error: `Failed to close issue #${iid}. Status: ${closeResponse.status}`,
|
||||||
|
issue: { id: issue.id, iid: issue.iid, title: issue.title, webUrl: issue.web_url }
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
const closedIssue = await closeResponse.json();
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'success',
|
||||||
|
action: 'close',
|
||||||
|
message: `Issue #${iid} has been closed successfully`,
|
||||||
|
issue: {
|
||||||
|
id: closedIssue.id,
|
||||||
|
iid: closedIssue.iid,
|
||||||
|
title: closedIssue.title,
|
||||||
|
webUrl: closedIssue.web_url,
|
||||||
|
state: closedIssue.state
|
||||||
|
}
|
||||||
|
}, null, 2) }] };
|
||||||
|
|
||||||
|
case "add-labels":
|
||||||
|
if (!labels || labels.length === 0) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
action: 'add-labels',
|
||||||
|
error: "No labels provided. Please specify labels to add using the 'labels' parameter.",
|
||||||
|
issue: { id: issue.id, iid: issue.iid, title: issue.title, webUrl: issue.web_url }
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
const currentLabels = issue.labels || [];
|
||||||
|
const newLabels = [...new Set([...currentLabels, ...labels])];
|
||||||
|
const labelsResponse = await fetch(`${service.gitlabUrl}/api/v4/projects/${projectId}/issues/${iid}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: service['getHeaders'](),
|
||||||
|
body: JSON.stringify({ labels: newLabels.join(',') })
|
||||||
|
});
|
||||||
|
if (!labelsResponse.ok) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
action: 'add-labels',
|
||||||
|
error: `Failed to add labels. Status: ${labelsResponse.status}`,
|
||||||
|
issue: { id: issue.id, iid: issue.iid, title: issue.title, webUrl: issue.web_url }
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
const labeledIssue = await labelsResponse.json();
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'success',
|
||||||
|
action: 'add-labels',
|
||||||
|
message: `Added labels to issue #${iid}`,
|
||||||
|
addedLabels: labels,
|
||||||
|
issue: {
|
||||||
|
id: labeledIssue.id,
|
||||||
|
iid: labeledIssue.iid,
|
||||||
|
title: labeledIssue.title,
|
||||||
|
webUrl: labeledIssue.web_url,
|
||||||
|
labels: labeledIssue.labels
|
||||||
|
}
|
||||||
|
}, null, 2) }] };
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
action: action,
|
||||||
|
error: `Unknown action "${action}"`
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
error: `Error managing issue #${iid}: ${String(e)}`
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 3: Complex Operation - Review Merge Request Code
|
||||||
|
|
||||||
|
**Tool Type**: Complex Operation (Pattern C)
|
||||||
|
|
||||||
|
### User Request
|
||||||
|
"Create a tool to add inline code review comments on merge requests with position tracking and duplicate detection"
|
||||||
|
|
||||||
|
### Generated File: `src/tools/gitlab/gitlab-review-merge-request-code.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { getGitLabService } from './gitlab-shared';
|
||||||
|
|
||||||
|
export function registerReviewMergeRequestCode(server: McpServer) {
|
||||||
|
server.registerTool(
|
||||||
|
"gitlab-review-merge-request-code",
|
||||||
|
{
|
||||||
|
title: "Review Merge Request Code",
|
||||||
|
description: "Add or update a code review comment on a merge request at a specific file and line position. This tool is designed for inline code reviews - it intelligently updates existing comments at the same position instead of creating duplicates. Requires diff SHA references (base, start, head) and file path with optional line numbers.",
|
||||||
|
inputSchema: {
|
||||||
|
projectId: z.number().describe("The project ID"),
|
||||||
|
mrIid: z.number().describe("The merge request IID"),
|
||||||
|
body: z.string().describe("The review comment body. Square brackets [] are allowed and commonly used in code references, markdown links, and examples."),
|
||||||
|
positionType: z.string().default("text").describe("Position type (text, image, etc.)"),
|
||||||
|
baseSha: z.string().describe("Base SHA for the diff"),
|
||||||
|
startSha: z.string().describe("Start SHA for the diff"),
|
||||||
|
headSha: z.string().describe("Head SHA for the diff"),
|
||||||
|
newPath: z.string().describe("Path to the file being reviewed. Square brackets [] are allowed in file paths."),
|
||||||
|
newLine: z.number().optional().describe("Line number in the new file (for line comments)"),
|
||||||
|
oldPath: z.string().optional().describe("Path to the old file (defaults to newPath). Square brackets [] are allowed in file paths."),
|
||||||
|
oldLine: z.number().optional().describe("Line number in the old file (for line comments)")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async ({ projectId, mrIid, body, positionType, baseSha, startSha, headSha, newPath, newLine, oldPath, oldLine }) => {
|
||||||
|
const pid = projectId as number;
|
||||||
|
const iid = mrIid as number;
|
||||||
|
const commentBody = body as string;
|
||||||
|
const posType = positionType as string;
|
||||||
|
const base = baseSha as string;
|
||||||
|
const start = startSha as string;
|
||||||
|
const head = headSha as string;
|
||||||
|
const path = newPath as string;
|
||||||
|
const line = newLine as number | undefined;
|
||||||
|
const oldFilePath = (oldPath as string | undefined) || path;
|
||||||
|
const oldFileLine = oldLine as number | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const service = await getGitLabService(server);
|
||||||
|
|
||||||
|
// Get existing discussions to check for existing review comments
|
||||||
|
const discussions = await service.getMrDiscussions(String(pid), iid);
|
||||||
|
|
||||||
|
// Find existing review comment at the same position
|
||||||
|
let existingDiscussion = null;
|
||||||
|
let existingNote = null;
|
||||||
|
|
||||||
|
for (const discussion of discussions) {
|
||||||
|
if (discussion.notes && discussion.notes.length > 0) {
|
||||||
|
const firstNote = discussion.notes[0];
|
||||||
|
|
||||||
|
// Check if the position matches our target position
|
||||||
|
if (firstNote.position &&
|
||||||
|
firstNote.position.new_path === path &&
|
||||||
|
firstNote.position.base_sha === base &&
|
||||||
|
firstNote.position.head_sha === head &&
|
||||||
|
firstNote.position.start_sha === start) {
|
||||||
|
|
||||||
|
// Check if line position matches (if specified)
|
||||||
|
const positionMatches = line !== undefined ?
|
||||||
|
firstNote.position.new_line === line :
|
||||||
|
!firstNote.position.new_line;
|
||||||
|
|
||||||
|
if (positionMatches) {
|
||||||
|
existingDiscussion = discussion;
|
||||||
|
existingNote = firstNote;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (existingNote && existingDiscussion) {
|
||||||
|
// Update existing comment
|
||||||
|
result = await service.updateMrDiscussionNote(
|
||||||
|
String(pid),
|
||||||
|
iid,
|
||||||
|
existingDiscussion.id,
|
||||||
|
existingNote.id,
|
||||||
|
commentBody
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
action: "updated",
|
||||||
|
discussion_id: existingDiscussion.id,
|
||||||
|
note_id: existingNote.id,
|
||||||
|
updated_note: result
|
||||||
|
})
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Create new comment
|
||||||
|
const position: any = {
|
||||||
|
position_type: posType,
|
||||||
|
base_sha: base,
|
||||||
|
start_sha: start,
|
||||||
|
head_sha: head,
|
||||||
|
new_path: path,
|
||||||
|
old_path: oldFilePath
|
||||||
|
};
|
||||||
|
|
||||||
|
if (line !== undefined) {
|
||||||
|
position.new_line = line;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldFileLine !== undefined) {
|
||||||
|
position.old_line = oldFileLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = { body: commentBody, position };
|
||||||
|
result = await service.addMrComments(String(pid), iid, data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
action: "created",
|
||||||
|
discussion: result
|
||||||
|
})
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({ type: "error", error: String(e) })
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 4: Simple CRUD - List Pipelines
|
||||||
|
|
||||||
|
**Tool Type**: Simple CRUD (Pattern A)
|
||||||
|
|
||||||
|
### User Request
|
||||||
|
"I need a tool to list all pipelines with status filtering and pagination"
|
||||||
|
|
||||||
|
### Generated File: `src/tools/gitlab/gitlab-list-pipelines.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { cleanGitLabHtmlContent } from '../../core/utils';
|
||||||
|
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
|
||||||
|
|
||||||
|
export function registerListPipelines(server: McpServer) {
|
||||||
|
server.registerTool(
|
||||||
|
"gitlab-list-pipelines",
|
||||||
|
{
|
||||||
|
title: "List Pipelines",
|
||||||
|
description: "Retrieve a list of pipelines for a GitLab project. Supports filtering by ref (branch/tag), status, and pagination. Returns pipeline information including ID, status, ref, commit details, and timestamps. Use this to monitor CI/CD pipeline execution, check build status, or find specific pipeline runs.",
|
||||||
|
inputSchema: {
|
||||||
|
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
|
||||||
|
ref: z.string().optional().describe("Filter pipelines by git reference (branch or tag name, e.g., 'main', 'develop')"),
|
||||||
|
status: z.enum(["running", "pending", "success", "failed", "canceled", "skipped", "manual"]).optional().describe("Filter pipelines by status"),
|
||||||
|
page: z.number().optional().describe("Page number for pagination (default: 1)"),
|
||||||
|
perPage: z.number().optional().describe("Number of pipelines per page (default: 20, max: 100)"),
|
||||||
|
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async ({ projectname, ref, status, page = 1, perPage = 20, format = "detailed" }) => {
|
||||||
|
try {
|
||||||
|
const projectName = projectname || await getProjectNameFromUser(server, false, "Please select the project for listing pipelines");
|
||||||
|
if (!projectName) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found or not selected. Please provide a valid project name." }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = await getGitLabService(server);
|
||||||
|
const projectId = await service.getProjectId(projectName);
|
||||||
|
if (!projectId) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.` }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: any = { page, per_page: perPage };
|
||||||
|
if (ref) options.ref = ref;
|
||||||
|
if (status) options.status = status;
|
||||||
|
|
||||||
|
const rawPipelines = await service.getPipelines(projectId, options);
|
||||||
|
if (!rawPipelines || rawPipelines.length === 0) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "info", message: `No pipelines found in project "${projectName}" with the specified filters.` }) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipelines = rawPipelines.map(p => cleanGitLabHtmlContent(p, []));
|
||||||
|
|
||||||
|
if (format === "concise") {
|
||||||
|
const summary = pipelines.map(p =>
|
||||||
|
`📋 Pipeline #${p.id} | ${p.status} | ${p.ref} | ${new Date(p.created_at).toLocaleDateString()}`
|
||||||
|
).join('\n');
|
||||||
|
return { content: [{ type: "text", text: `📋 Found ${pipelines.length} pipeline(s) in "${projectName}":\n\n${summary}` }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }] };
|
||||||
|
} catch (e) {
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Error listing pipelines: ${String(e)}. Please check your GitLab connection and permissions.` }) }] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Patterns Summary
|
||||||
|
|
||||||
|
### Tool Pattern Selection Guide
|
||||||
|
|
||||||
|
| Tool Type | When to Use | Key Features | Example |
|
||||||
|
|-----------|-------------|--------------|---------|
|
||||||
|
| **Simple CRUD** | Single operation on resource | projectname, format, emojis | `gitlab-get-issue` |
|
||||||
|
| **Multi-Action** | Multiple operations on same resource | action enum, structured responses | `gitlab-manage-issue` |
|
||||||
|
| **Complex** | Advanced logic, discussions, position-based | Custom parameters, specialized logic | `gitlab-review-merge-request-code` |
|
||||||
|
|
||||||
|
### Response Format Patterns
|
||||||
|
|
||||||
|
**Simple CRUD - Concise**:
|
||||||
|
```typescript
|
||||||
|
if (format === "concise") {
|
||||||
|
return { content: [{ type: "text", text:
|
||||||
|
`🔍 Resource #${id}: ${title}\n` +
|
||||||
|
`📊 Status: ${state}\n` +
|
||||||
|
`🔗 URL: ${web_url}`
|
||||||
|
}] };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-Action - Structured**:
|
||||||
|
```typescript
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'success',
|
||||||
|
action: 'close',
|
||||||
|
message: 'Issue closed successfully',
|
||||||
|
issue: { /* key fields */ }
|
||||||
|
}, null, 2) }] };
|
||||||
|
```
|
||||||
|
|
||||||
|
**Complex - Custom**:
|
||||||
|
```typescript
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
action: "updated",
|
||||||
|
discussion_id: "...",
|
||||||
|
updated_note: {...}
|
||||||
|
}) }] };
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
// Operation logic
|
||||||
|
} catch (e) {
|
||||||
|
// Simple CRUD
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
error: `Error: ${String(e)}`
|
||||||
|
}) }] };
|
||||||
|
|
||||||
|
// Multi-Action
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({
|
||||||
|
status: 'failure',
|
||||||
|
error: `Error: ${String(e)}`
|
||||||
|
}, null, 2) }] };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Comparison Table
|
||||||
|
|
||||||
|
| Feature | Simple CRUD | Multi-Action | Complex |
|
||||||
|
|---------|-------------|--------------|---------|
|
||||||
|
| projectname param | ✅ Optional | ✅ Optional | ❌ May use projectId |
|
||||||
|
| format param | ✅ Required | ❌ Not used | ❌ Not used |
|
||||||
|
| action enum | ❌ Not used | ✅ Required | ❌ Custom |
|
||||||
|
| Emoji output | ✅ Concise format | ❌ Not used | ❌ Not used |
|
||||||
|
| HTML cleaning | ✅ Always | ✅ Always | ⚠️ If applicable |
|
||||||
|
| Response type | JSON or text | Structured JSON | Custom |
|
||||||
|
| Direct fetch API | ❌ Use service | ✅ Often used | ✅ If needed |
|
||||||
|
| Complexity | Low | Medium | High |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**All examples follow the project's standardized patterns and conventions from CLAUDE.md!**
|
||||||
158
skills/rxswift-memory-check/SKILL.md
Normal file
158
skills/rxswift-memory-check/SKILL.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
name: rxswift-memory-check
|
||||||
|
description: Quick RxSwift memory leak detection for iOS. Finds missing dispose bags, retain cycles, and strong self references. Use when debugging memory issues, checking Observable subscriptions, or investigating retain cycles in RxSwift code.
|
||||||
|
allowed-tools: Read, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# RxSwift Memory Leak Detector
|
||||||
|
|
||||||
|
Fast, focused check for RxSwift memory management issues in iOS code.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- "memory leak", "retain cycle", "dispose bag"
|
||||||
|
- "RxSwift memory issues", "check subscriptions"
|
||||||
|
- "[weak self]", "memory management"
|
||||||
|
- Debugging memory problems or crashes
|
||||||
|
|
||||||
|
## Quick Check Process
|
||||||
|
|
||||||
|
### Step 1: Find RxSwift Subscriptions
|
||||||
|
|
||||||
|
Use Grep to locate all Observable subscriptions:
|
||||||
|
- Pattern: `\.subscribe\(`
|
||||||
|
- Check surrounding code for proper disposal
|
||||||
|
|
||||||
|
### Step 2: Verify Each Subscription
|
||||||
|
|
||||||
|
For every `.subscribe(`:
|
||||||
|
1. ✅ Has `.disposed(by: disposeBag)`
|
||||||
|
2. ✅ Uses `[weak self]` or `[unowned self]` in closures
|
||||||
|
3. ✅ DisposeBag is a property, not local variable
|
||||||
|
|
||||||
|
### Step 3: Check Common Patterns
|
||||||
|
|
||||||
|
#### 🔴 Pattern 1: Missing Disposal
|
||||||
|
```swift
|
||||||
|
viewModel.data
|
||||||
|
.subscribe(onNext: { data in })
|
||||||
|
// MISSING: .disposed(by: disposeBag)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔴 Pattern 2: Retain Cycle
|
||||||
|
```swift
|
||||||
|
viewModel.data
|
||||||
|
.subscribe(onNext: { data in
|
||||||
|
self.updateUI(data) // Strong self!
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔴 Pattern 3: Local DisposeBag
|
||||||
|
```swift
|
||||||
|
func loadData() {
|
||||||
|
let disposeBag = DisposeBag() // Local variable!
|
||||||
|
// Cancels immediately when function ends
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Generate Report
|
||||||
|
|
||||||
|
Focused report with:
|
||||||
|
- Critical issues by severity
|
||||||
|
- File locations and line numbers
|
||||||
|
- Current vs. fixed code
|
||||||
|
- Impact assessment
|
||||||
|
- Recommended fixes
|
||||||
|
|
||||||
|
## Search Patterns
|
||||||
|
|
||||||
|
### Find subscriptions without disposal
|
||||||
|
```
|
||||||
|
Pattern: \.subscribe\(
|
||||||
|
Context: Check next 5 lines for .disposed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find strong self references
|
||||||
|
```
|
||||||
|
Pattern: subscribe.*\{[^[]*self\.
|
||||||
|
Context: -A 3 -B 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find local DisposeBag declarations
|
||||||
|
```
|
||||||
|
Pattern: let disposeBag = DisposeBag\(\)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# RxSwift Memory Check Report
|
||||||
|
|
||||||
|
## Critical Issues: X
|
||||||
|
|
||||||
|
### 1. Missing Disposal - MEMORY LEAK
|
||||||
|
**File**: `PaymentViewModel.swift:45`
|
||||||
|
**Risk**: Memory accumulation, eventual crash
|
||||||
|
|
||||||
|
**Current**:
|
||||||
|
```swift
|
||||||
|
// Missing disposal
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
```swift
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: [Explanation]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
🔴 Critical: X (memory leaks/retain cycles)
|
||||||
|
⚠️ Warnings: X (could use weak self)
|
||||||
|
|
||||||
|
## Files Status
|
||||||
|
✅ Clean files
|
||||||
|
⚠️ Files with warnings
|
||||||
|
🔴 Files with critical issues
|
||||||
|
```
|
||||||
|
|
||||||
|
## DisposeBag Best Practices
|
||||||
|
|
||||||
|
✅ **Correct**: Property-level DisposeBag
|
||||||
|
```swift
|
||||||
|
class ViewModel {
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Wrong**: Local DisposeBag
|
||||||
|
```swift
|
||||||
|
func loadData() {
|
||||||
|
let disposeBag = DisposeBag() // Cancels immediately!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use `weak` vs `unowned`
|
||||||
|
|
||||||
|
- **Default**: Always use `[weak self]` (safer)
|
||||||
|
- **Rare**: Use `[unowned self]` only if 100% sure self outlives subscription
|
||||||
|
|
||||||
|
## Quick Fix Guide
|
||||||
|
|
||||||
|
1. **Add Missing Disposal**: `.disposed(by: disposeBag)`
|
||||||
|
2. **Add Weak Self**: `[weak self]` in closure
|
||||||
|
3. **Move DisposeBag**: To property level
|
||||||
|
|
||||||
|
## Testing the Fix
|
||||||
|
|
||||||
|
Suggest verification:
|
||||||
|
1. Run Instruments with Leaks template
|
||||||
|
2. Navigate to/from screens multiple times
|
||||||
|
3. Check Debug Memory Graph for cycles
|
||||||
|
4. Verify view controllers deallocate
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
**Detailed Examples**: See `examples.md` for extensive code samples and scenarios.
|
||||||
536
skills/rxswift-memory-check/examples.md
Normal file
536
skills/rxswift-memory-check/examples.md
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
# RxSwift Memory Check Examples
|
||||||
|
|
||||||
|
Detailed examples of memory leaks, retain cycles, and proper RxSwift memory management.
|
||||||
|
|
||||||
|
## Critical Issue Examples
|
||||||
|
|
||||||
|
### Example 1: Missing Disposal (Memory Leak)
|
||||||
|
|
||||||
|
#### ❌ Problem Code
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
private let paymentUC: PaymentUseCase
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
func processPayment() {
|
||||||
|
paymentUC.execute(amount: paymentAmount.value)
|
||||||
|
.subscribe(onNext: { [weak self] result in
|
||||||
|
self?.handleResult(result)
|
||||||
|
})
|
||||||
|
// ❌ MISSING: .disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: Subscription never releases, accumulates in memory
|
||||||
|
**Impact**: Memory grows over time, eventually crashes app
|
||||||
|
**Symptoms**: Increasing memory usage, app slowdown
|
||||||
|
|
||||||
|
#### ✅ Fixed Code
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
private let paymentUC: PaymentUseCase
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
func processPayment() {
|
||||||
|
paymentUC.execute(amount: paymentAmount.value)
|
||||||
|
.subscribe(onNext: { [weak self] result in
|
||||||
|
self?.handleResult(result)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag) // ✅ Added
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**: Add `.disposed(by: disposeBag)` to every subscription
|
||||||
|
**Result**: Subscription properly cleaned up when ViewModel deallocates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example 2: Retain Cycle (Strong Self Reference)
|
||||||
|
|
||||||
|
#### ❌ Problem Code
|
||||||
|
```swift
|
||||||
|
class StoresViewModel: BaseViewModel<StoresState> {
|
||||||
|
private let storesUC: StoresUseCase
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
let stores = BehaviorRelay<[Store]>(value: [])
|
||||||
|
|
||||||
|
func loadStores() {
|
||||||
|
storesUC.getStores()
|
||||||
|
.subscribe(onNext: { stores in
|
||||||
|
// ❌ Strong self reference - retain cycle!
|
||||||
|
self.stores.accept(stores)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: Closure captures self strongly, creating retain cycle
|
||||||
|
**Impact**: ViewModel never deallocates, memory leak
|
||||||
|
**Symptoms**: View controllers don't deallocate, memory grows
|
||||||
|
**Detection**: Xcode Debug Memory Graph shows cycle
|
||||||
|
|
||||||
|
#### ✅ Fixed Code
|
||||||
|
```swift
|
||||||
|
class StoresViewModel: BaseViewModel<StoresState> {
|
||||||
|
private let storesUC: StoresUseCase
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
let stores = BehaviorRelay<[Store]>(value: [])
|
||||||
|
|
||||||
|
func loadStores() {
|
||||||
|
storesUC.getStores()
|
||||||
|
.subscribe(onNext: { [weak self] stores in
|
||||||
|
// ✅ Weak self - no retain cycle
|
||||||
|
self?.stores.accept(stores)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**: Use `[weak self]` in closure capture list
|
||||||
|
**Result**: ViewModel can deallocate properly, no memory leak
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example 3: Local DisposeBag (Early Cancellation)
|
||||||
|
|
||||||
|
#### ❌ Problem Code
|
||||||
|
```swift
|
||||||
|
class TransactionViewModel: BaseViewModel<TransactionState> {
|
||||||
|
func loadTransactions() {
|
||||||
|
let disposeBag = DisposeBag() // ❌ Local variable!
|
||||||
|
|
||||||
|
transactionUC.getTransactions()
|
||||||
|
.subscribe(onNext: { [weak self] transactions in
|
||||||
|
self?.handleTransactions(transactions)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// ❌ disposeBag deallocates here, cancels subscription immediately!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: DisposeBag deallocates when function exits, canceling subscription
|
||||||
|
**Impact**: Observable never completes, callbacks never fire
|
||||||
|
**Symptoms**: Data doesn't load, UI doesn't update
|
||||||
|
|
||||||
|
#### ✅ Fixed Code
|
||||||
|
```swift
|
||||||
|
class TransactionViewModel: BaseViewModel<TransactionState> {
|
||||||
|
private let disposeBag = DisposeBag() // ✅ Property
|
||||||
|
|
||||||
|
func loadTransactions() {
|
||||||
|
transactionUC.getTransactions()
|
||||||
|
.subscribe(onNext: { [weak self] transactions in
|
||||||
|
self?.handleTransactions(transactions)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag) // ✅ Uses property
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**: Make DisposeBag a class property, not local variable
|
||||||
|
**Result**: Subscription lives as long as the ViewModel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example 4: Multiple DisposeBags (Anti-Pattern)
|
||||||
|
|
||||||
|
#### ❌ Problem Code
|
||||||
|
```swift
|
||||||
|
class DashboardViewModel: BaseViewModel<DashboardState> {
|
||||||
|
private var searchDisposeBag = DisposeBag() // ❌ Anti-pattern
|
||||||
|
private var dataDisposeBag = DisposeBag() // ❌ Anti-pattern
|
||||||
|
private var uiDisposeBag = DisposeBag() // ❌ Anti-pattern
|
||||||
|
|
||||||
|
func search(query: String) {
|
||||||
|
searchService.search(query)
|
||||||
|
.subscribe(onNext: { results in })
|
||||||
|
.disposed(by: searchDisposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadData() {
|
||||||
|
dataService.loadData()
|
||||||
|
.subscribe(onNext: { data in })
|
||||||
|
.disposed(by: dataDisposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: Unnecessary complexity, harder to manage subscriptions
|
||||||
|
**Impact**: Confusing code, potential for errors
|
||||||
|
|
||||||
|
#### ✅ Fixed Code
|
||||||
|
```swift
|
||||||
|
class DashboardViewModel: BaseViewModel<DashboardState> {
|
||||||
|
private let disposeBag = DisposeBag() // ✅ Single DisposeBag
|
||||||
|
|
||||||
|
func search(query: String) {
|
||||||
|
searchService.search(query)
|
||||||
|
.subscribe(onNext: { results in })
|
||||||
|
.disposed(by: disposeBag) // ✅ Same bag
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadData() {
|
||||||
|
dataService.loadData()
|
||||||
|
.subscribe(onNext: { data in })
|
||||||
|
.disposed(by: disposeBag) // ✅ Same bag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**: Use single DisposeBag per class
|
||||||
|
**Result**: Simpler code, all subscriptions disposed together
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Examples
|
||||||
|
|
||||||
|
### Example 5: Proper RxSwift Memory Management
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
// Dependencies
|
||||||
|
private let paymentUC: PaymentUseCase
|
||||||
|
private let validationService: ValidationService
|
||||||
|
|
||||||
|
// DisposeBag property (not local!)
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
// State
|
||||||
|
let paymentAmount = BehaviorRelay<String>(value: "")
|
||||||
|
let isProcessing = BehaviorRelay<Bool>(value: false)
|
||||||
|
|
||||||
|
init(paymentUC: PaymentUseCase, validationService: ValidationService) {
|
||||||
|
self.paymentUC = paymentUC
|
||||||
|
self.validationService = validationService
|
||||||
|
super.init()
|
||||||
|
setupBindings()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupBindings() {
|
||||||
|
// ✅ Proper disposal with weak self
|
||||||
|
paymentAmount
|
||||||
|
.map { [weak self] amount in
|
||||||
|
self?.validationService.validateAmount(amount) ?? false
|
||||||
|
}
|
||||||
|
.subscribe(onNext: { [weak self] isValid in
|
||||||
|
self?.setState(isValid ? .valid : .invalid)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processPayment() {
|
||||||
|
isProcessing.accept(true)
|
||||||
|
|
||||||
|
paymentUC.execute(amount: paymentAmount.value)
|
||||||
|
.subscribeOn(ConcurrentScheduler.background)
|
||||||
|
.observeOn(MainScheduler.instance)
|
||||||
|
.subscribe(
|
||||||
|
onNext: { [weak self] result in
|
||||||
|
self?.handleSuccess(result)
|
||||||
|
},
|
||||||
|
onError: { [weak self] error in
|
||||||
|
self?.handleError(error)
|
||||||
|
},
|
||||||
|
onCompleted: { [weak self] in
|
||||||
|
self?.isProcessing.accept(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.disposed(by: disposeBag) // ✅ Always disposed
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleSuccess(_ result: PaymentResult) {
|
||||||
|
setState(.success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleError(_ error: Error) {
|
||||||
|
setState(.error(error))
|
||||||
|
isProcessing.accept(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Points**:
|
||||||
|
- ✅ DisposeBag is a property
|
||||||
|
- ✅ Every subscription uses `.disposed(by:)`
|
||||||
|
- ✅ Every closure uses `[weak self]`
|
||||||
|
- ✅ Proper error handling
|
||||||
|
- ✅ Correct scheduler usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example 6: ViewController with Proper Bindings
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class PaymentViewController: UIViewController {
|
||||||
|
@IBOutlet weak var amountTextField: UITextField!
|
||||||
|
@IBOutlet weak var confirmButton: UIButton!
|
||||||
|
@IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
|
||||||
|
|
||||||
|
private let viewModel: PaymentViewModel
|
||||||
|
private let disposeBag = DisposeBag() // ✅ Property
|
||||||
|
|
||||||
|
init(viewModel: PaymentViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("Use dependency injection")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
setupUI()
|
||||||
|
bindViewModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
title = "Payment"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindViewModel() {
|
||||||
|
// Input: TextField → ViewModel
|
||||||
|
amountTextField.rx.text
|
||||||
|
.orEmpty
|
||||||
|
.bind(to: viewModel.paymentAmount)
|
||||||
|
.disposed(by: disposeBag) // ✅ Disposed
|
||||||
|
|
||||||
|
// Input: Button tap → ViewModel action
|
||||||
|
confirmButton.rx.tap
|
||||||
|
.subscribe(onNext: { [weak self] in
|
||||||
|
self?.viewModel.processPayment()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag) // ✅ Disposed
|
||||||
|
|
||||||
|
// Output: ViewModel → UI
|
||||||
|
viewModel.isProcessing
|
||||||
|
.bind(to: loadingIndicator.rx.isAnimating)
|
||||||
|
.disposed(by: disposeBag) // ✅ Disposed
|
||||||
|
|
||||||
|
viewModel.isProcessing
|
||||||
|
.map { !$0 }
|
||||||
|
.bind(to: confirmButton.rx.isEnabled)
|
||||||
|
.disposed(by: disposeBag) // ✅ Disposed
|
||||||
|
|
||||||
|
// State handling
|
||||||
|
viewModel.getState()
|
||||||
|
.compactMap { $0 }
|
||||||
|
.observeOn(MainScheduler.instance)
|
||||||
|
.subscribe(onNext: { [weak self] state in
|
||||||
|
self?.handleState(state.name)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag) // ✅ Disposed
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleState(_ state: PaymentState) {
|
||||||
|
switch state {
|
||||||
|
case .success(let result):
|
||||||
|
showSuccessAlert(result)
|
||||||
|
case .error(let error):
|
||||||
|
showErrorAlert(error)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Points**:
|
||||||
|
- ✅ Single DisposeBag for all bindings
|
||||||
|
- ✅ All UI bindings properly disposed
|
||||||
|
- ✅ Weak self in subscribe closures
|
||||||
|
- ✅ Clean separation of concerns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Timer/Interval Observable
|
||||||
|
|
||||||
|
#### ❌ Problem
|
||||||
|
```swift
|
||||||
|
func startTimer() {
|
||||||
|
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
|
||||||
|
.subscribe(onNext: { [weak self] tick in
|
||||||
|
self?.updateTime(tick)
|
||||||
|
})
|
||||||
|
// ❌ No disposal - timer runs forever!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ Solution
|
||||||
|
```swift
|
||||||
|
class TimerViewModel {
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
func startTimer() {
|
||||||
|
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
|
||||||
|
.subscribe(onNext: { [weak self] tick in
|
||||||
|
self?.updateTime(tick)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag) // ✅ Stops when ViewModel deallocates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 2: Network Requests
|
||||||
|
|
||||||
|
#### ❌ Problem
|
||||||
|
```swift
|
||||||
|
func loadData() {
|
||||||
|
networkService.fetchData()
|
||||||
|
.subscribe(onNext: { data in
|
||||||
|
self.data = data // ❌ Strong self
|
||||||
|
})
|
||||||
|
// ❌ No disposal
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ Solution
|
||||||
|
```swift
|
||||||
|
class DataViewModel {
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
func loadData() {
|
||||||
|
networkService.fetchData()
|
||||||
|
.subscribe(
|
||||||
|
onNext: { [weak self] data in
|
||||||
|
self?.data = data
|
||||||
|
},
|
||||||
|
onError: { [weak self] error in
|
||||||
|
self?.handleError(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 3: Chained Observables
|
||||||
|
|
||||||
|
#### ❌ Problem
|
||||||
|
```swift
|
||||||
|
func processData() {
|
||||||
|
fetchData()
|
||||||
|
.flatMap { data in
|
||||||
|
return self.transform(data) // ❌ Strong self
|
||||||
|
}
|
||||||
|
.flatMap { transformed in
|
||||||
|
return self.save(transformed) // ❌ Strong self
|
||||||
|
}
|
||||||
|
.subscribe(onNext: { result in
|
||||||
|
self.handleResult(result) // ❌ Strong self
|
||||||
|
})
|
||||||
|
// ❌ No disposal
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ Solution
|
||||||
|
```swift
|
||||||
|
class DataProcessor {
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
func processData() {
|
||||||
|
fetchData()
|
||||||
|
.flatMap { [weak self] data -> Observable<TransformedData> in
|
||||||
|
guard let self = self else { return .empty() }
|
||||||
|
return self.transform(data)
|
||||||
|
}
|
||||||
|
.flatMap { [weak self] transformed -> Observable<Result> in
|
||||||
|
guard let self = self else { return .empty() }
|
||||||
|
return self.save(transformed)
|
||||||
|
}
|
||||||
|
.subscribe(
|
||||||
|
onNext: { [weak self] result in
|
||||||
|
self?.handleResult(result)
|
||||||
|
},
|
||||||
|
onError: { [weak self] error in
|
||||||
|
self?.handleError(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detection Tools
|
||||||
|
|
||||||
|
### Using Xcode Debug Memory Graph
|
||||||
|
|
||||||
|
1. Run app in Simulator/Device
|
||||||
|
2. Navigate to screen with suspected leak
|
||||||
|
3. Pop back
|
||||||
|
4. Xcode → Debug → View Memory Graph
|
||||||
|
5. Look for objects that should have deallocated
|
||||||
|
6. Check retain cycle graph
|
||||||
|
|
||||||
|
### Using Instruments
|
||||||
|
|
||||||
|
1. Product → Profile
|
||||||
|
2. Choose "Leaks" template
|
||||||
|
3. Record while using app
|
||||||
|
4. Navigate between screens
|
||||||
|
5. Check for red leak indicators
|
||||||
|
6. Inspect stack traces
|
||||||
|
|
||||||
|
### Manual Verification
|
||||||
|
|
||||||
|
Add `deinit` to ViewModels and ViewControllers:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||||
|
deinit {
|
||||||
|
print("✅ PaymentViewModel deallocated") // Should print when leaving screen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentViewController: UIViewController {
|
||||||
|
deinit {
|
||||||
|
print("✅ PaymentViewController deallocated") // Should print when popping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `deinit` doesn't print, you have a memory leak!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Memory Management Checklist
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## RxSwift Memory Check
|
||||||
|
|
||||||
|
For each Observable subscription:
|
||||||
|
- [ ] Has `.disposed(by: disposeBag)`
|
||||||
|
- [ ] Uses `[weak self]` in closures
|
||||||
|
- [ ] DisposeBag is a property (not local)
|
||||||
|
- [ ] No strong reference cycles
|
||||||
|
- [ ] Error handling present
|
||||||
|
- [ ] deinit prints when tested
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Patterns
|
||||||
|
|
||||||
|
| Pattern | Issue | Fix |
|
||||||
|
|---------|-------|-----|
|
||||||
|
| Missing disposal | Memory leak | Add `.disposed(by: disposeBag)` |
|
||||||
|
| Strong self | Retain cycle | Use `[weak self]` |
|
||||||
|
| Local DisposeBag | Early cancel | Make it a property |
|
||||||
|
| Multiple bags | Complexity | Use single DisposeBag |
|
||||||
|
| No error handling | Crashes | Add `onError` handler |
|
||||||
301
skills/skill-builder/SKILL.md
Normal file
301
skills/skill-builder/SKILL.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
---
|
||||||
|
name: skill-builder
|
||||||
|
description: Create new Claude Code agent skills. Automatically analyzes requirements and generates complete skill with proper structure, triggers, and tools. Use when you want to "create a skill", "build a new skill", "make a skill for", or automate repetitive tasks.
|
||||||
|
allowed-tools: Read, Write, Glob, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill Builder
|
||||||
|
|
||||||
|
Autonomously create new Claude Code agent skills with intelligent analysis and generation.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- "create a skill for...", "build a new skill"
|
||||||
|
- "make a skill that...", "I need a skill to..."
|
||||||
|
- "generate a skill for..."
|
||||||
|
|
||||||
|
## Autonomous Creation Process
|
||||||
|
|
||||||
|
### Step 1: Analyze User Request
|
||||||
|
|
||||||
|
**Automatically infer from the request:**
|
||||||
|
|
||||||
|
1. **Task Type Detection**:
|
||||||
|
- Review/Check → Code Review Skill (Read, Grep, Glob)
|
||||||
|
- Generate/Create → Code Generator (Read, Write, Glob)
|
||||||
|
- Analyze/Report → Analyzer (Read, Grep, Glob, Bash)
|
||||||
|
- Refactor/Improve → Refactoring Assistant (Read, Write, Grep, Glob)
|
||||||
|
- Test → Test Assistant (Read, Write, Bash)
|
||||||
|
- Document → Documentation Generator (Read, Write, Glob)
|
||||||
|
|
||||||
|
2. **Skill Name Derivation**:
|
||||||
|
- Extract key technology/concept from request
|
||||||
|
- Format: `[technology]-[action]` (e.g., "check iOS performance" → `ios-performance-check`)
|
||||||
|
- Keep lowercase with hyphens, max 64 chars
|
||||||
|
|
||||||
|
3. **Trigger Keywords Extraction**:
|
||||||
|
- Parse user's language for natural phrases
|
||||||
|
- Add common variations and synonyms
|
||||||
|
- Include file type mentions if applicable
|
||||||
|
|
||||||
|
4. **Tool Requirements**:
|
||||||
|
- Read-only tasks → `Read, Grep, Glob`
|
||||||
|
- Generation tasks → `Read, Write, Glob`
|
||||||
|
- Command/build tasks → `Read, Write, Bash`
|
||||||
|
- Complex workflows → No restrictions
|
||||||
|
|
||||||
|
5. **Output Format Selection**:
|
||||||
|
- Review/Check → Report with issues and fixes
|
||||||
|
- Generate → File creation confirmation with examples
|
||||||
|
- Analyze → Metrics report with visualizations
|
||||||
|
- Refactor → Proposal with before/after
|
||||||
|
- Test → Test results and coverage report
|
||||||
|
- Document → Generated documentation preview
|
||||||
|
|
||||||
|
### Step 2: Smart Question Strategy
|
||||||
|
|
||||||
|
**Only ask if truly ambiguous:**
|
||||||
|
- Multiple valid approaches? → Ask which approach
|
||||||
|
- Unclear scope? → Ask for scope clarification
|
||||||
|
- Critical choices? → Ask for user preference
|
||||||
|
|
||||||
|
**Never ask if inferable:**
|
||||||
|
- Skill name → Auto-generate from request
|
||||||
|
- Basic tools → Auto-select based on task type
|
||||||
|
- Common triggers → Auto-generate from context
|
||||||
|
- Output format → Auto-select based on skill type
|
||||||
|
|
||||||
|
### Step 3: Auto-Generate Skill Structure
|
||||||
|
|
||||||
|
**Automatically create complete skill with:**
|
||||||
|
|
||||||
|
#### Auto-Generated Skill Name
|
||||||
|
Format: `[technology]-[action]`
|
||||||
|
Examples:
|
||||||
|
- "check iOS naming" → `ios-naming-check`
|
||||||
|
- "generate React component" → `react-component-generator`
|
||||||
|
- "analyze dependencies" → `dependency-analyzer`
|
||||||
|
|
||||||
|
#### Auto-Generated Description
|
||||||
|
Template:
|
||||||
|
```
|
||||||
|
[Verb] [what] [context]. [Specifics]. Use when [trigger1], [trigger2], "[quoted phrases]", or [patterns].
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-include:
|
||||||
|
- Specific action extracted from request
|
||||||
|
- What it checks/generates/analyzes
|
||||||
|
- Trigger phrases derived from user's language
|
||||||
|
- File types if mentioned
|
||||||
|
- Natural language variations
|
||||||
|
|
||||||
|
#### Auto-Selected Tools
|
||||||
|
Based on detected task type:
|
||||||
|
- Review/Check → `Read, Grep, Glob`
|
||||||
|
- Generate → `Read, Write, Glob`
|
||||||
|
- Analyze with commands → `Read, Grep, Glob, Bash`
|
||||||
|
- Refactor → `Read, Write, Grep, Glob`
|
||||||
|
- Test with execution → `Read, Write, Bash`
|
||||||
|
|
||||||
|
#### Auto-Generated Content Structure
|
||||||
|
|
||||||
|
Select appropriate template based on task type (from templates.md):
|
||||||
|
- Code Review → Template 1
|
||||||
|
- Code Generator → Template 2
|
||||||
|
- Analyzer/Reporter → Template 3
|
||||||
|
- Refactoring Assistant → Template 4
|
||||||
|
- Test Assistant → Template 5
|
||||||
|
- Documentation Generator → Template 6
|
||||||
|
|
||||||
|
Populate template with:
|
||||||
|
- Auto-generated name, description, tools
|
||||||
|
- Task-specific process steps
|
||||||
|
- Relevant output format
|
||||||
|
- Examples from similar skills (from examples.md)
|
||||||
|
|
||||||
|
### Step 4: Generate and Validate
|
||||||
|
|
||||||
|
**Auto-create directory structure:**
|
||||||
|
```bash
|
||||||
|
mkdir -p .claude/skills/[auto-generated-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auto-generate files:**
|
||||||
|
|
||||||
|
1. **`SKILL.md`** (Required, Concise)
|
||||||
|
- Frontmatter (name, description, allowed-tools)
|
||||||
|
- When to Activate (trigger phrases)
|
||||||
|
- Process steps (core workflow)
|
||||||
|
- Output format (template structure)
|
||||||
|
- Keep under 150 lines - core instructions only!
|
||||||
|
|
||||||
|
2. **`templates.md`** (Optional, for code/structure templates)
|
||||||
|
- Code templates
|
||||||
|
- File structure templates
|
||||||
|
- Boilerplate examples
|
||||||
|
- Use when skill generates code or files
|
||||||
|
|
||||||
|
3. **`examples.md`** (Optional, for detailed examples)
|
||||||
|
- Real-world usage examples
|
||||||
|
- Before/after code samples
|
||||||
|
- Complex scenarios
|
||||||
|
- Use when skill needs detailed guidance
|
||||||
|
|
||||||
|
4. **`standards.md`** (Optional, for rules/guidelines)
|
||||||
|
- Coding standards
|
||||||
|
- Naming conventions
|
||||||
|
- Best practices
|
||||||
|
- Reference documentation
|
||||||
|
- Use when skill enforces specific rules
|
||||||
|
|
||||||
|
**Separation principle:**
|
||||||
|
- SKILL.md = Concise instructions (what to do, how to do it)
|
||||||
|
- Separate files = Supporting details (templates, examples, references)
|
||||||
|
|
||||||
|
**Auto-validate:**
|
||||||
|
- ✓ Valid YAML frontmatter with `---` delimiters
|
||||||
|
- ✓ Name lowercase with hyphens
|
||||||
|
- ✓ Description specific with quoted triggers
|
||||||
|
- ✓ Tools appropriate for task type
|
||||||
|
- ✓ Process steps clear and actionable
|
||||||
|
- ✓ SKILL.md concise (under 150 lines)
|
||||||
|
- ✓ Extra content moved to separate files
|
||||||
|
|
||||||
|
### Step 5: Present and Test
|
||||||
|
|
||||||
|
**Show user:**
|
||||||
|
```markdown
|
||||||
|
✅ Created skill: [name]
|
||||||
|
|
||||||
|
📁 Location: `.claude/skills/[name]/SKILL.md`
|
||||||
|
|
||||||
|
🎯 Try these phrases:
|
||||||
|
- "[trigger phrase 1]"
|
||||||
|
- "[trigger phrase 2]"
|
||||||
|
- "[trigger phrase 3]"
|
||||||
|
|
||||||
|
📖 Description: [generated description]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference: Task Types
|
||||||
|
|
||||||
|
| Type | Tools | Output | Example Name |
|
||||||
|
|------|-------|--------|--------------|
|
||||||
|
| Review | Read, Grep, Glob | Issues report | `security-review` |
|
||||||
|
| Generator | Read, Write, Glob | New files | `component-generator` |
|
||||||
|
| Analyzer | Read, Grep, Glob, Bash | Metrics report | `dependency-analyzer` |
|
||||||
|
| Refactor | Read, Write, Grep, Glob | Modified files | `extract-method` |
|
||||||
|
| Test | Read, Write, Bash | Test results | `test-runner` |
|
||||||
|
| Document | Read, Write, Glob | Documentation | `api-docs-generator` |
|
||||||
|
|
||||||
|
## Auto-Generation Examples
|
||||||
|
|
||||||
|
### Example 1: User says "create a skill to check TODO comments"
|
||||||
|
**Auto-analysis:**
|
||||||
|
- Task type: Review (check/find)
|
||||||
|
- Name: `todo-finder`
|
||||||
|
- Tools: `Read, Grep, Glob`
|
||||||
|
- Triggers: "find todos", "check todos", "show todos", "list fixmes"
|
||||||
|
- Output: Report organized by priority
|
||||||
|
|
||||||
|
**Action:** Auto-generate complete skill, create files, show confirmation
|
||||||
|
|
||||||
|
### Example 2: User says "I need to generate React components"
|
||||||
|
**Auto-analysis:**
|
||||||
|
- Task type: Generator (create/generate)
|
||||||
|
- Name: `react-component-generator`
|
||||||
|
- Tools: `Read, Write, Glob`
|
||||||
|
- Triggers: "create component", "generate component", "new component"
|
||||||
|
- Output: Component files with tests
|
||||||
|
|
||||||
|
**Action:** Auto-generate complete skill, create files, show confirmation
|
||||||
|
|
||||||
|
### Example 3: User says "make a skill for iOS performance issues"
|
||||||
|
**Auto-analysis:**
|
||||||
|
- Task type: Analyzer (check performance)
|
||||||
|
- Name: `ios-performance-check`
|
||||||
|
- Tools: `Read, Grep, Glob, Bash`
|
||||||
|
- Triggers: "check performance", "performance issues", "slow code"
|
||||||
|
- Output: Performance report with fixes
|
||||||
|
|
||||||
|
**Action:** Auto-generate complete skill, create files, show confirmation
|
||||||
|
|
||||||
|
## Standard Workflow
|
||||||
|
|
||||||
|
When user requests a skill:
|
||||||
|
|
||||||
|
1. **Analyze** request → Detect task type, extract key concepts
|
||||||
|
2. **Generate** skill name, description, triggers automatically
|
||||||
|
3. **Select** appropriate template and tools
|
||||||
|
4. **Create** `.claude/skills/[name]/SKILL.md` with complete content
|
||||||
|
5. **Validate** frontmatter, structure, triggers
|
||||||
|
6. **Present** summary with test phrases
|
||||||
|
|
||||||
|
**No questions asked unless truly ambiguous!**
|
||||||
|
|
||||||
|
## Creation Output Format
|
||||||
|
|
||||||
|
After auto-generating skill, show:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
✅ Skill Created: [name]
|
||||||
|
|
||||||
|
📁 Files created:
|
||||||
|
- `.claude/skills/[name]/SKILL.md` (core instructions)
|
||||||
|
- `.claude/skills/[name]/templates.md` (if applicable)
|
||||||
|
- `.claude/skills/[name]/examples.md` (if applicable)
|
||||||
|
- `.claude/skills/[name]/standards.md` (if applicable)
|
||||||
|
|
||||||
|
🔧 Type: [task-type]
|
||||||
|
🛠️ Tools: [tool-list]
|
||||||
|
📄 Lines: [line-count] (SKILL.md is concise!)
|
||||||
|
|
||||||
|
🎯 Test with these phrases:
|
||||||
|
- "[natural trigger 1]"
|
||||||
|
- "[natural trigger 2]"
|
||||||
|
- "[natural trigger 3]"
|
||||||
|
|
||||||
|
📖 Full description:
|
||||||
|
[Generated description with all triggers]
|
||||||
|
|
||||||
|
✅ Ready to use! Try one of the test phrases above.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Internal References
|
||||||
|
|
||||||
|
Use these for generation (don't show to user):
|
||||||
|
- **templates.md**: 6 skill templates for different task types
|
||||||
|
- **examples.md**: 5 complete real-world examples
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
1. **Be autonomous**: Infer everything possible from the user's request
|
||||||
|
2. **Ask minimally**: Only ask if genuinely ambiguous (approach, scope, critical choices)
|
||||||
|
3. **Generate completely**: Create full SKILL.md with all sections
|
||||||
|
4. **Validate automatically**: Check frontmatter, structure, triggers before presenting
|
||||||
|
5. **Present clearly**: Show what was created and how to test it
|
||||||
|
|
||||||
|
## Important Rules
|
||||||
|
|
||||||
|
### Auto-Generation
|
||||||
|
- **Auto-generate** skill name from request (lowercase-with-hyphens)
|
||||||
|
- **Auto-detect** task type to select template and tools
|
||||||
|
- **Auto-extract** trigger phrases from user's language
|
||||||
|
- **Auto-create** description with specific triggers in quotes
|
||||||
|
- **Auto-select** appropriate tools based on task type
|
||||||
|
|
||||||
|
### Quality Standards
|
||||||
|
- **No generic names**: Always use specific technology/action names
|
||||||
|
- **No vague descriptions**: Always include specific triggers and file types
|
||||||
|
- **Valid YAML**: Always use `---` delimiters and proper frontmatter format
|
||||||
|
|
||||||
|
### File Organization
|
||||||
|
- **SKILL.md must be concise**: Under 150 lines, core instructions only
|
||||||
|
- **Separate templates**: Move code templates to `templates.md`
|
||||||
|
- **Separate examples**: Move detailed examples to `examples.md`
|
||||||
|
- **Separate standards**: Move rules/guidelines to `standards.md`
|
||||||
|
- **Clear separation**: Instructions in SKILL.md, details in other files
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready for autonomous skill generation!** Just tell me what skill you need.
|
||||||
Reference in New Issue
Block a user