opencode-skills-collection 3.0.42 → 3.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/bundled-skills/.antigravity-install-manifest.json +3 -1
- package/bundled-skills/2slides-ppt-generator/SKILL.md +8 -18
- package/bundled-skills/accesslint-diff/SKILL.md +5 -2
- package/bundled-skills/android-dev/SKILL.md +524 -0
- package/bundled-skills/android-dev/references/flutter.md +269 -0
- package/bundled-skills/android-dev/references/hybrid.md +158 -0
- package/bundled-skills/android-dev/references/java-android.md +586 -0
- package/bundled-skills/android-dev/references/kmm.md +206 -0
- package/bundled-skills/android-dev/references/native-android.md +239 -0
- package/bundled-skills/android-dev/references/react-native.md +242 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/event-staffing-ordering/SKILL.md +2 -17
- package/bundled-skills/unship/SKILL.md +138 -0
- package/package.json +1 -1
- package/skills_index.json +44 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Kotlin Multiplatform (KMM) Reference
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
project/
|
|
7
|
+
├── shared/ # Shared KMM module
|
|
8
|
+
│ ├── src/
|
|
9
|
+
│ │ ├── commonMain/kotlin/ # Business logic, domain, data
|
|
10
|
+
│ │ │ ├── domain/
|
|
11
|
+
│ │ │ │ ├── model/
|
|
12
|
+
│ │ │ │ ├── repository/ # Interfaces
|
|
13
|
+
│ │ │ │ └── usecase/
|
|
14
|
+
│ │ │ ├── data/
|
|
15
|
+
│ │ │ │ ├── remote/ # Ktor client + DTOs
|
|
16
|
+
│ │ │ │ ├── local/ # SQLDelight DAOs
|
|
17
|
+
│ │ │ │ └── repository/ # Implementations
|
|
18
|
+
│ │ │ └── di/ # Koin modules
|
|
19
|
+
│ │ ├── androidMain/kotlin/ # Android-specific actual implementations
|
|
20
|
+
│ │ └── iosMain/kotlin/ # iOS-specific actual (if needed)
|
|
21
|
+
│ └── build.gradle.kts
|
|
22
|
+
├── androidApp/ # Android app module
|
|
23
|
+
│ ├── src/main/java/
|
|
24
|
+
│ │ ├── ui/ # Jetpack Compose screens
|
|
25
|
+
│ │ ├── presentation/ # Android ViewModels
|
|
26
|
+
│ │ └── di/ # Android-specific DI
|
|
27
|
+
│ └── build.gradle.kts
|
|
28
|
+
└── build.gradle.kts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Shared Module: Ktor HTTP Client
|
|
32
|
+
|
|
33
|
+
```kotlin
|
|
34
|
+
// commonMain
|
|
35
|
+
expect fun httpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient
|
|
36
|
+
|
|
37
|
+
// androidMain
|
|
38
|
+
actual fun httpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient =
|
|
39
|
+
HttpClient(OkHttp) {
|
|
40
|
+
config(this)
|
|
41
|
+
engine { addInterceptor(/* logging, auth */) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Shared usage
|
|
45
|
+
val client = httpClient {
|
|
46
|
+
install(ContentNegotiation) { json() }
|
|
47
|
+
install(HttpTimeout) { requestTimeoutMillis = 10_000 }
|
|
48
|
+
defaultRequest {
|
|
49
|
+
url(BuildKonfig.BASE_URL)
|
|
50
|
+
header(HttpHeaders.ContentType, ContentType.Application.Json)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## SQLDelight Setup
|
|
56
|
+
|
|
57
|
+
```sql
|
|
58
|
+
-- ItemEntity.sq
|
|
59
|
+
CREATE TABLE ItemEntity (
|
|
60
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
61
|
+
title TEXT NOT NULL,
|
|
62
|
+
updatedAt INTEGER NOT NULL DEFAULT 0
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
selectAll:
|
|
66
|
+
SELECT * FROM ItemEntity ORDER BY updatedAt DESC;
|
|
67
|
+
|
|
68
|
+
upsertItem:
|
|
69
|
+
INSERT OR REPLACE INTO ItemEntity (id, title, updatedAt)
|
|
70
|
+
VALUES (?, ?, ?);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```kotlin
|
|
74
|
+
// commonMain — Database driver expect/actual
|
|
75
|
+
expect class DatabaseDriverFactory {
|
|
76
|
+
fun createDriver(): SqlDriver
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// androidMain
|
|
80
|
+
actual class DatabaseDriverFactory(private val context: Context) {
|
|
81
|
+
actual fun createDriver(): SqlDriver =
|
|
82
|
+
AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Shared Repository
|
|
87
|
+
|
|
88
|
+
```kotlin
|
|
89
|
+
// commonMain
|
|
90
|
+
class ItemRepositoryImpl(
|
|
91
|
+
private val remoteSource: ItemRemoteDataSource,
|
|
92
|
+
private val localSource: ItemLocalDataSource,
|
|
93
|
+
) : ItemRepository {
|
|
94
|
+
|
|
95
|
+
override fun observeItems(): Flow<List<Item>> =
|
|
96
|
+
localSource.observeAll().map { entities ->
|
|
97
|
+
entities.map { it.toDomain() }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override suspend fun refreshItems(): Result<Unit> = runCatching {
|
|
101
|
+
val items = remoteSource.fetchItems()
|
|
102
|
+
localSource.upsertAll(items.map { it.toEntity() })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Android ViewModel consuming shared Flow
|
|
108
|
+
|
|
109
|
+
```kotlin
|
|
110
|
+
@HiltViewModel
|
|
111
|
+
class HomeViewModel @Inject constructor(
|
|
112
|
+
private val observeItems: ObserveItemsUseCase, // from shared module
|
|
113
|
+
private val refreshItems: RefreshItemsUseCase // from shared module
|
|
114
|
+
) : ViewModel() {
|
|
115
|
+
|
|
116
|
+
val uiState = observeItems()
|
|
117
|
+
.map { HomeUiState.Success(it) as HomeUiState }
|
|
118
|
+
.stateIn(
|
|
119
|
+
scope = viewModelScope,
|
|
120
|
+
started = SharingStarted.WhileSubscribed(5_000),
|
|
121
|
+
initialValue = HomeUiState.Loading
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Koin DI (Shared + Android)
|
|
127
|
+
|
|
128
|
+
```kotlin
|
|
129
|
+
// commonMain — shared Koin modules
|
|
130
|
+
val sharedModule = module {
|
|
131
|
+
single { DatabaseDriverFactory(get()) }
|
|
132
|
+
single { AppDatabase(get<DatabaseDriverFactory>().createDriver()) }
|
|
133
|
+
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
|
|
134
|
+
factory { ObserveItemsUseCase(get()) }
|
|
135
|
+
factory { RefreshItemsUseCase(get()) }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// androidApp — Android-specific module
|
|
139
|
+
val androidModule = module {
|
|
140
|
+
single<Context> { androidApplication() }
|
|
141
|
+
viewModel { HomeViewModel(get(), get()) }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Application class
|
|
145
|
+
class MyApp : Application() {
|
|
146
|
+
override fun onCreate() {
|
|
147
|
+
super.onCreate()
|
|
148
|
+
startKoin {
|
|
149
|
+
androidContext(this@MyApp)
|
|
150
|
+
modules(sharedModule, androidModule)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Key Gradle Dependencies (shared/build.gradle.kts)
|
|
157
|
+
|
|
158
|
+
```kotlin
|
|
159
|
+
kotlin {
|
|
160
|
+
androidTarget()
|
|
161
|
+
// Add other targets as needed (jvm, iosArm64, etc.)
|
|
162
|
+
|
|
163
|
+
sourceSets {
|
|
164
|
+
commonMain.dependencies {
|
|
165
|
+
implementation(libs.ktor.client.core)
|
|
166
|
+
implementation(libs.ktor.client.content.negotiation)
|
|
167
|
+
implementation(libs.ktor.serialization.kotlinx.json)
|
|
168
|
+
implementation(libs.sqldelight.runtime)
|
|
169
|
+
implementation(libs.koin.core)
|
|
170
|
+
implementation(libs.kotlinx.coroutines.core)
|
|
171
|
+
implementation(libs.kotlinx.serialization.json)
|
|
172
|
+
}
|
|
173
|
+
androidMain.dependencies {
|
|
174
|
+
implementation(libs.ktor.client.okhttp)
|
|
175
|
+
implementation(libs.sqldelight.android.driver)
|
|
176
|
+
implementation(libs.koin.android)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Compose Multiplatform (for shared UI)
|
|
183
|
+
|
|
184
|
+
Use when you want to share UI across Android + Desktop + Web:
|
|
185
|
+
|
|
186
|
+
```kotlin
|
|
187
|
+
// commonMain — shared composable
|
|
188
|
+
@Composable
|
|
189
|
+
fun HomeScreenContent(
|
|
190
|
+
state: HomeUiState,
|
|
191
|
+
onRetry: () -> Unit
|
|
192
|
+
) {
|
|
193
|
+
when (state) {
|
|
194
|
+
is HomeUiState.Loading -> CircularProgressIndicator()
|
|
195
|
+
is HomeUiState.Success -> ItemList(state.items)
|
|
196
|
+
is HomeUiState.Error -> ErrorView(state.message, onRetry)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// androidApp — wraps with Android ViewModel
|
|
201
|
+
@Composable
|
|
202
|
+
fun HomeScreen(viewModel: HomeViewModel = koinViewModel()) {
|
|
203
|
+
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
|
204
|
+
HomeScreenContent(state, onRetry = viewModel::refresh)
|
|
205
|
+
}
|
|
206
|
+
```
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Native Android Reference (Kotlin + Jetpack Compose)
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
app/
|
|
7
|
+
├── src/
|
|
8
|
+
│ ├── main/
|
|
9
|
+
│ │ ├── AndroidManifest.xml
|
|
10
|
+
│ │ ├── java/com.example.app/
|
|
11
|
+
│ │ │ ├── MyApp.kt # Application class, Hilt entry point
|
|
12
|
+
│ │ │ ├── MainActivity.kt # Single activity, NavHost host
|
|
13
|
+
│ │ │ ├── ui/
|
|
14
|
+
│ │ │ │ ├── theme/ # MaterialTheme, Color, Type, Shape
|
|
15
|
+
│ │ │ │ ├── components/ # Shared design system composables
|
|
16
|
+
│ │ │ │ └── feature/
|
|
17
|
+
│ │ │ │ ├── home/
|
|
18
|
+
│ │ │ │ │ ├── HomeScreen.kt
|
|
19
|
+
│ │ │ │ │ ├── HomeViewModel.kt
|
|
20
|
+
│ │ │ │ │ └── HomeUiState.kt
|
|
21
|
+
│ │ │ ├── domain/
|
|
22
|
+
│ │ │ │ ├── model/ # Domain models (pure Kotlin, no Android deps)
|
|
23
|
+
│ │ │ │ ├── repository/ # Interfaces only
|
|
24
|
+
│ │ │ │ └── usecase/ # One class per use case
|
|
25
|
+
│ │ │ ├── data/
|
|
26
|
+
│ │ │ │ ├── remote/ # Retrofit services, DTOs, mappers
|
|
27
|
+
│ │ │ │ ├── local/ # Room DB, DAOs, entities
|
|
28
|
+
│ │ │ │ └── repository/ # Repository implementations
|
|
29
|
+
│ │ │ └── di/ # Hilt modules
|
|
30
|
+
│ └── test/ # Unit tests
|
|
31
|
+
│ └── androidTest/ # Instrumented tests
|
|
32
|
+
├── build.gradle.kts
|
|
33
|
+
└── proguard-rules.pro
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## ViewModel Pattern
|
|
37
|
+
|
|
38
|
+
```kotlin
|
|
39
|
+
// UiState — sealed class for exhaustive when()
|
|
40
|
+
sealed class HomeUiState {
|
|
41
|
+
object Loading : HomeUiState()
|
|
42
|
+
data class Success(val items: List<Item>) : HomeUiState()
|
|
43
|
+
data class Error(val message: String) : HomeUiState()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// UiEvent — one-shot events (navigation, snackbars)
|
|
47
|
+
sealed class HomeUiEvent {
|
|
48
|
+
data class NavigateTo(val route: String) : HomeUiEvent()
|
|
49
|
+
data class ShowSnackbar(val message: String) : HomeUiEvent()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@HiltViewModel
|
|
53
|
+
class HomeViewModel @Inject constructor(
|
|
54
|
+
private val getItemsUseCase: GetItemsUseCase
|
|
55
|
+
) : ViewModel() {
|
|
56
|
+
|
|
57
|
+
private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading)
|
|
58
|
+
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
|
|
59
|
+
|
|
60
|
+
private val _uiEvent = Channel<HomeUiEvent>()
|
|
61
|
+
val uiEvent = _uiEvent.receiveAsFlow()
|
|
62
|
+
|
|
63
|
+
init { loadItems() }
|
|
64
|
+
|
|
65
|
+
fun loadItems() {
|
|
66
|
+
viewModelScope.launch {
|
|
67
|
+
_uiState.value = HomeUiState.Loading
|
|
68
|
+
getItemsUseCase()
|
|
69
|
+
.onSuccess { _uiState.value = HomeUiState.Success(it) }
|
|
70
|
+
.onFailure { _uiState.value = HomeUiState.Error(it.message ?: "Unknown error") }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Repository Pattern
|
|
77
|
+
|
|
78
|
+
```kotlin
|
|
79
|
+
// Interface in domain layer
|
|
80
|
+
interface ItemRepository {
|
|
81
|
+
fun observeItems(): Flow<List<Item>>
|
|
82
|
+
suspend fun refreshItems(): Result<Unit>
|
|
83
|
+
suspend fun getItemById(id: String): Result<Item>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Implementation in data layer
|
|
87
|
+
class ItemRepositoryImpl @Inject constructor(
|
|
88
|
+
private val remoteSource: ItemRemoteDataSource,
|
|
89
|
+
private val localSource: ItemLocalDataSource,
|
|
90
|
+
private val mapper: ItemMapper
|
|
91
|
+
) : ItemRepository {
|
|
92
|
+
|
|
93
|
+
override fun observeItems(): Flow<List<Item>> =
|
|
94
|
+
localSource.observeAll().map { mapper.toDomain(it) }
|
|
95
|
+
|
|
96
|
+
override suspend fun refreshItems(): Result<Unit> = runCatching {
|
|
97
|
+
val dto = remoteSource.fetchItems()
|
|
98
|
+
localSource.insertAll(mapper.toEntity(dto))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override suspend fun getItemById(id: String): Result<Item> = runCatching {
|
|
102
|
+
// Example implementation fetching from local cache
|
|
103
|
+
val entity = localSource.getById(id) ?: throw Exception("Item not found")
|
|
104
|
+
mapper.toDomain(entity)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Compose Screen
|
|
110
|
+
|
|
111
|
+
```kotlin
|
|
112
|
+
@Composable
|
|
113
|
+
fun HomeScreen(
|
|
114
|
+
viewModel: HomeViewModel = hiltViewModel(),
|
|
115
|
+
onNavigate: (String) -> Unit
|
|
116
|
+
) {
|
|
117
|
+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
118
|
+
val snackbarHostState = remember { SnackbarHostState() }
|
|
119
|
+
|
|
120
|
+
// One-shot event handling
|
|
121
|
+
LaunchedEffect(Unit) {
|
|
122
|
+
viewModel.uiEvent.collect { event ->
|
|
123
|
+
when (event) {
|
|
124
|
+
is HomeUiEvent.NavigateTo -> onNavigate(event.route)
|
|
125
|
+
is HomeUiEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.message)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
|
|
131
|
+
when (val state = uiState) {
|
|
132
|
+
is HomeUiState.Loading -> LoadingContent()
|
|
133
|
+
is HomeUiState.Success -> HomeContent(state.items, Modifier.padding(padding))
|
|
134
|
+
is HomeUiState.Error -> ErrorContent(state.message, onRetry = viewModel::loadItems)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Room Database
|
|
141
|
+
|
|
142
|
+
```kotlin
|
|
143
|
+
@Entity(tableName = "items")
|
|
144
|
+
data class ItemEntity(
|
|
145
|
+
@PrimaryKey val id: String,
|
|
146
|
+
val title: String,
|
|
147
|
+
val updatedAt: Long = System.currentTimeMillis()
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@Dao
|
|
151
|
+
interface ItemDao {
|
|
152
|
+
@Query("SELECT * FROM items ORDER BY updatedAt DESC")
|
|
153
|
+
fun observeAll(): Flow<List<ItemEntity>>
|
|
154
|
+
|
|
155
|
+
@Upsert
|
|
156
|
+
suspend fun upsertAll(items: List<ItemEntity>)
|
|
157
|
+
|
|
158
|
+
@Query("DELETE FROM items")
|
|
159
|
+
suspend fun deleteAll()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@Database(entities = [ItemEntity::class], version = 1, exportSchema = true)
|
|
163
|
+
abstract class AppDatabase : RoomDatabase() {
|
|
164
|
+
abstract fun itemDao(): ItemDao
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Hilt DI Setup
|
|
169
|
+
|
|
170
|
+
```kotlin
|
|
171
|
+
@Module
|
|
172
|
+
@InstallIn(SingletonComponent::class)
|
|
173
|
+
object NetworkModule {
|
|
174
|
+
@Provides @Singleton
|
|
175
|
+
fun provideRetrofit(): Retrofit = Retrofit.Builder()
|
|
176
|
+
.baseUrl(BuildConfig.API_BASE_URL)
|
|
177
|
+
.addConverterFactory(GsonConverterFactory.create())
|
|
178
|
+
.client(buildOkHttpClient())
|
|
179
|
+
.build()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@Module
|
|
183
|
+
@InstallIn(SingletonComponent::class)
|
|
184
|
+
abstract class RepositoryModule {
|
|
185
|
+
@Binds @Singleton
|
|
186
|
+
abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Key Dependencies (libs.versions.toml)
|
|
191
|
+
|
|
192
|
+
```toml
|
|
193
|
+
[versions]
|
|
194
|
+
kotlin = "2.0.0"
|
|
195
|
+
compose-bom = "2024.06.00"
|
|
196
|
+
hilt = "2.51"
|
|
197
|
+
room = "2.6.1"
|
|
198
|
+
retrofit = "2.11.0"
|
|
199
|
+
coroutines = "1.8.1"
|
|
200
|
+
lifecycle = "2.8.2"
|
|
201
|
+
|
|
202
|
+
[libraries]
|
|
203
|
+
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
|
204
|
+
compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
|
205
|
+
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
|
206
|
+
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
|
207
|
+
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
|
208
|
+
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
|
209
|
+
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
|
210
|
+
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
|
211
|
+
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Testing Setup
|
|
215
|
+
|
|
216
|
+
```kotlin
|
|
217
|
+
// ViewModel unit test
|
|
218
|
+
@OptIn(ExperimentalCoroutinesApi::class)
|
|
219
|
+
class HomeViewModelTest {
|
|
220
|
+
@get:Rule val mainDispatcherRule = MainDispatcherRule()
|
|
221
|
+
|
|
222
|
+
private val getItemsUseCase = mockk<GetItemsUseCase>()
|
|
223
|
+
private lateinit var viewModel: HomeViewModel
|
|
224
|
+
|
|
225
|
+
@BeforeEach
|
|
226
|
+
fun setup() { viewModel = HomeViewModel(getItemsUseCase) }
|
|
227
|
+
|
|
228
|
+
@Test
|
|
229
|
+
fun `loadItems emits Success when use case succeeds`() = runTest {
|
|
230
|
+
val items = listOf(Item("1", "Test"))
|
|
231
|
+
coEvery { getItemsUseCase() } returns Result.success(items)
|
|
232
|
+
|
|
233
|
+
viewModel.uiState.test {
|
|
234
|
+
skipItems(1) // Loading
|
|
235
|
+
assertThat(awaitItem()).isEqualTo(HomeUiState.Success(items))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# React Native Reference (TypeScript)
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
src/
|
|
7
|
+
├── app/
|
|
8
|
+
│ ├── App.tsx # Root component, providers
|
|
9
|
+
│ ├── navigation/ # React Navigation stacks + types
|
|
10
|
+
│ └── store/ # RTK store setup
|
|
11
|
+
├── features/
|
|
12
|
+
│ └── home/
|
|
13
|
+
│ ├── api/ # RTK Query endpoints
|
|
14
|
+
│ ├── components/ # Screen-specific components
|
|
15
|
+
│ ├── hooks/ # Feature-level custom hooks
|
|
16
|
+
│ ├── screens/ # Screen components
|
|
17
|
+
│ ├── store/ # Zustand slice or RTK slice
|
|
18
|
+
│ └── types.ts # Feature types
|
|
19
|
+
├── shared/
|
|
20
|
+
│ ├── components/ # Design system components
|
|
21
|
+
│ ├── hooks/ # Shared hooks
|
|
22
|
+
│ ├── theme/ # Colors, typography, spacing constants
|
|
23
|
+
│ └── utils/ # Utilities
|
|
24
|
+
└── services/
|
|
25
|
+
├── api/ # Axios/fetch client + interceptors
|
|
26
|
+
└── storage/ # MMKV wrapper
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Navigation Setup (React Navigation v7)
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
export type RootStackParamList = {
|
|
33
|
+
Auth: undefined;
|
|
34
|
+
Home: undefined;
|
|
35
|
+
Detail: { id: string };
|
|
36
|
+
Settings: undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type RootStackScreenProps<T extends keyof RootStackParamList> =
|
|
40
|
+
NativeStackScreenProps<RootStackParamList, T>;
|
|
41
|
+
|
|
42
|
+
const Stack = createNativeStackNavigator<RootStackParamList>();
|
|
43
|
+
|
|
44
|
+
export const RootNavigator = () => {
|
|
45
|
+
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
|
49
|
+
{isLoggedIn ? (
|
|
50
|
+
<>
|
|
51
|
+
<Stack.Screen name="Home" component={HomeScreen} />
|
|
52
|
+
<Stack.Screen name="Detail" component={DetailScreen} />
|
|
53
|
+
</>
|
|
54
|
+
) : (
|
|
55
|
+
<Stack.Screen name="Auth" component={AuthScreen} />
|
|
56
|
+
)}
|
|
57
|
+
</Stack.Navigator>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## State Management (Zustand + React Query)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// Client state — Zustand
|
|
66
|
+
interface AuthState {
|
|
67
|
+
token: string | null;
|
|
68
|
+
isLoggedIn: boolean;
|
|
69
|
+
setToken: (token: string) => void;
|
|
70
|
+
logout: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const useAuthStore = create<AuthState>()(
|
|
74
|
+
persist(
|
|
75
|
+
(set) => ({
|
|
76
|
+
token: null,
|
|
77
|
+
isLoggedIn: false,
|
|
78
|
+
setToken: (token) => set({ token, isLoggedIn: true }),
|
|
79
|
+
logout: () => set({ token: null, isLoggedIn: false }),
|
|
80
|
+
}),
|
|
81
|
+
{ name: 'auth-storage', storage: createJSONStorage(() => mmkvStorage) }
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Server state — React Query
|
|
86
|
+
export const useItems = () =>
|
|
87
|
+
useQuery({
|
|
88
|
+
queryKey: ['items'],
|
|
89
|
+
queryFn: itemsApi.getAll,
|
|
90
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const useRefreshItems = () =>
|
|
94
|
+
useMutation({
|
|
95
|
+
mutationFn: itemsApi.refresh,
|
|
96
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Screen Pattern
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
type HomeScreenProps = RootStackScreenProps<'Home'>;
|
|
104
|
+
|
|
105
|
+
export const HomeScreen: FC<HomeScreenProps> = ({ navigation }) => {
|
|
106
|
+
const { data: items, isLoading, isError, refetch } = useItems();
|
|
107
|
+
|
|
108
|
+
if (isLoading) return <LoadingView />;
|
|
109
|
+
if (isError) return <ErrorView onRetry={refetch} />;
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<SafeAreaView style={styles.container}>
|
|
113
|
+
<FlatList
|
|
114
|
+
data={items}
|
|
115
|
+
keyExtractor={(item) => item.id}
|
|
116
|
+
renderItem={({ item }) => (
|
|
117
|
+
<ItemCard
|
|
118
|
+
item={item}
|
|
119
|
+
onPress={() => navigation.navigate('Detail', { id: item.id })}
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
ListEmptyComponent={<EmptyView />}
|
|
123
|
+
refreshControl={
|
|
124
|
+
<RefreshControl refreshing={isLoading} onRefresh={refetch} />
|
|
125
|
+
}
|
|
126
|
+
/>
|
|
127
|
+
</SafeAreaView>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## API Client (Axios with interceptors)
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const apiClient = axios.create({
|
|
136
|
+
baseURL: Config.API_BASE_URL,
|
|
137
|
+
timeout: 10_000,
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Auth token injection
|
|
142
|
+
apiClient.interceptors.request.use((config) => {
|
|
143
|
+
const token = useAuthStore.getState().token;
|
|
144
|
+
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
145
|
+
return config;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Token refresh on 401
|
|
149
|
+
apiClient.interceptors.response.use(
|
|
150
|
+
(res) => res,
|
|
151
|
+
async (error: AxiosError) => {
|
|
152
|
+
if (error.response?.status === 401) {
|
|
153
|
+
const newToken = await refreshToken();
|
|
154
|
+
if (newToken) {
|
|
155
|
+
useAuthStore.getState().setToken(newToken);
|
|
156
|
+
return apiClient(error.config!);
|
|
157
|
+
}
|
|
158
|
+
useAuthStore.getState().logout();
|
|
159
|
+
}
|
|
160
|
+
return Promise.reject(error);
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## API Response Validation (Zod)
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const ItemSchema = z.object({
|
|
169
|
+
id: z.string(),
|
|
170
|
+
title: z.string(),
|
|
171
|
+
description: z.string().optional(),
|
|
172
|
+
createdAt: z.string().datetime(),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const ItemsResponseSchema = z.array(ItemSchema);
|
|
176
|
+
type Item = z.infer<typeof ItemSchema>;
|
|
177
|
+
|
|
178
|
+
const getItems = async (): Promise<Item[]> => {
|
|
179
|
+
const { data } = await apiClient.get('/items');
|
|
180
|
+
return ItemsResponseSchema.parse(data); // throws ZodError on invalid shape
|
|
181
|
+
};
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Key Dependencies
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"dependencies": {
|
|
189
|
+
"react-native": "0.74.x",
|
|
190
|
+
"@react-navigation/native": "^7.0.0",
|
|
191
|
+
"@react-navigation/native-stack": "^7.0.0",
|
|
192
|
+
"@tanstack/react-query": "^5.45.0",
|
|
193
|
+
"zustand": "^4.5.4",
|
|
194
|
+
"axios": "^1.7.2",
|
|
195
|
+
"zod": "^3.23.8",
|
|
196
|
+
"react-native-mmkv": "^2.12.2",
|
|
197
|
+
"react-native-safe-area-context": "^4.10.1",
|
|
198
|
+
"react-native-screens": "^3.32.0"
|
|
199
|
+
},
|
|
200
|
+
"devDependencies": {
|
|
201
|
+
"typescript": "^5.4.5",
|
|
202
|
+
"@testing-library/react-native": "^12.5.1",
|
|
203
|
+
"msw": "^2.3.1",
|
|
204
|
+
"jest": "^29.7.0"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## New Architecture (Bridgeless) Notes
|
|
210
|
+
- Enable New Architecture in `android/gradle.properties`: `newArchEnabled=true`
|
|
211
|
+
- Use TurboModules for native modules; avoid legacy NativeModules API
|
|
212
|
+
- Use Fabric for custom native views
|
|
213
|
+
- Test with Hermes JS engine always enabled
|
|
214
|
+
|
|
215
|
+
## Performance Tips
|
|
216
|
+
- Use `useCallback` + `memo` on `renderItem` / list item components
|
|
217
|
+
- `FlatList` `windowSize`, `initialNumToRender`, `maxToRenderPerBatch` tuned
|
|
218
|
+
- Avoid anonymous inline functions in JSX
|
|
219
|
+
- `InteractionManager.runAfterInteractions` for heavy post-navigation work
|
|
220
|
+
- `react-native-reanimated` for 60fps animations (runs on UI thread)
|
|
221
|
+
|
|
222
|
+
## Testing
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
describe('HomeScreen', () => {
|
|
226
|
+
it('shows items when query succeeds', async () => {
|
|
227
|
+
server.use(
|
|
228
|
+
http.get(`${API_URL}/items`, () =>
|
|
229
|
+
HttpResponse.json([{ id: '1', title: 'Test Item' }])
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const { getByText } = render(
|
|
234
|
+
<QueryClientProvider client={testQueryClient}>
|
|
235
|
+
<HomeScreen navigation={mockNavigation} route={mockRoute} />
|
|
236
|
+
</QueryClientProvider>
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
expect(await findByText('Test Item')).toBeTruthy();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
```
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Jetski/Cortex + Gemini Integration Guide
|
|
3
|
-
description: "Use antigravity-awesome-skills with Jetski/Cortex without hitting context-window overflow with 1,
|
|
3
|
+
description: "Use antigravity-awesome-skills with Jetski/Cortex without hitting context-window overflow with 1,527+ skills."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Jetski/Cortex + Gemini: safe integration with 1,
|
|
6
|
+
# Jetski/Cortex + Gemini: safe integration with 1,527+ skills
|
|
7
7
|
|
|
8
8
|
This guide shows how to integrate the `antigravity-awesome-skills` repository with an agent based on **Jetski/Cortex + Gemini** (or similar frameworks) **without exceeding the model context window**.
|
|
9
9
|
|
|
@@ -23,7 +23,7 @@ Never do:
|
|
|
23
23
|
- concatenate all `SKILL.md` content into a single system prompt;
|
|
24
24
|
- re-inject the entire library for **every** request.
|
|
25
25
|
|
|
26
|
-
With 1,
|
|
26
|
+
With 1,527+ skills, this approach fills the context window before user messages are even added, causing truncation.
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|