android-ai-skills 0.1.0 → 1.2.0
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/CHANGELOG.md +29 -0
- package/README.md +178 -119
- package/bin/android-ai-skills.js +317 -66
- package/kotlin-coroutines-best-practices/SKILL.md +39 -0
- package/kotlin-coroutines-best-practices/references/anti_patterns.md +30 -0
- package/kotlin-coroutines-best-practices/references/channels.md +23 -0
- package/kotlin-coroutines-best-practices/references/dispatchers.md +24 -0
- package/kotlin-coroutines-best-practices/references/error_handling.md +25 -0
- package/kotlin-coroutines-best-practices/references/flow_operators.md +41 -0
- package/kotlin-coroutines-best-practices/references/flow_types.md +33 -0
- package/kotlin-coroutines-best-practices/references/structured_concurrency.md +19 -0
- package/kotlin-coroutines-best-practices/references/testing.md +28 -0
- package/package.json +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
# [1.2.0](https://github.com/noloman/Android-AI-skills/compare/v1.1.0...v1.2.0) (2026-02-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **ci:** fix trusted publishing — upgrade npm and add repository field ([cc93884](https://github.com/noloman/Android-AI-skills/commit/cc938841c51b3c22633e66030d84ddeb3982a744))
|
|
7
|
+
* **ci:** remove registry-url that interferes with OIDC token exchange ([eed08b9](https://github.com/noloman/Android-AI-skills/commit/eed08b9f7f0343d4b3081861df7aa1eb6ea1dd4b))
|
|
8
|
+
* **ci:** switch to npm trusted publishing via OIDC ([f74d8ce](https://github.com/noloman/Android-AI-skills/commit/f74d8ce182ddb1e16439dc1a5e4dbf9aba4a4654))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add kotlin-coroutines-best-practices skill ([6e002e9](https://github.com/noloman/Android-AI-skills/commit/6e002e917c4b88b77fc749a5abd87ab6300526a7))
|
|
14
|
+
|
|
15
|
+
# [1.1.0](https://github.com/noloman/Android-AI-skills/compare/v1.0.0...v1.1.0) (2026-02-12)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* add universal AI coding assistant support (9 tools) ([44bd7b5](https://github.com/noloman/Android-AI-skills/commit/44bd7b51dc5ce081bf6d3f37673093aa5caae1d8))
|
|
21
|
+
|
|
22
|
+
# 1.0.0 (2026-02-12)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* **ci:** remove registry-url to avoid NPM_TOKEN conflict ([b749728](https://github.com/noloman/Android-AI-skills/commit/b74972832cd2367562d895dfd2df2b3e07b69bab))
|
|
28
|
+
* **ci:** upgrade Node.js to 22 for semantic-release compatibility ([5805e8d](https://github.com/noloman/Android-AI-skills/commit/5805e8d1c7e449841aaf55b73145d3047772805c))
|
|
29
|
+
|
|
1
30
|
# Changelog
|
|
2
31
|
|
|
3
32
|
All notable changes to this project will be documented in this file.
|
package/README.md
CHANGED
|
@@ -1,18 +1,43 @@
|
|
|
1
|
-
[](https://github.com/noloman/android-ai-skills/actions)
|
|
2
2
|
[](https://www.npmjs.com/package/android-ai-skills)
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# Android AI Architecture Skills
|
|
6
6
|
|
|
7
7
|
Opinionated, production-grade AI skills for Android, Kotlin Multiplatform (KMP),
|
|
8
8
|
and Compose Multiplatform projects.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Works with **Codex, Claude Code, GitHub Copilot, Cursor, Windsurf, Cline,
|
|
11
|
+
JetBrains AI, Amazon Q & Aider**.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## Supported AI Tools
|
|
16
|
+
|
|
17
|
+
### Global install (home directory)
|
|
18
|
+
|
|
19
|
+
| Tool | Path | Format |
|
|
20
|
+
|------|------|--------|
|
|
21
|
+
| Codex | `~/.codex/skills/<name>/SKILL.md` | Directory copy |
|
|
22
|
+
| Claude Code | `~/.claude/rules/<name>.md` | Flattened markdown |
|
|
23
|
+
|
|
24
|
+
### Project-level (`init` command)
|
|
25
|
+
|
|
26
|
+
| Tool | Path | Format |
|
|
27
|
+
|------|------|--------|
|
|
28
|
+
| Codex | `AGENTS.md` | Single markdown file |
|
|
29
|
+
| Claude Code | `CLAUDE.md` | Single markdown file |
|
|
30
|
+
| GitHub Copilot | `.github/copilot-instructions.md` | Single markdown file |
|
|
31
|
+
| Cursor | `.cursor/rules/<name>.mdc` | MDC per skill |
|
|
32
|
+
| Windsurf | `.windsurfrules` | Single markdown file |
|
|
33
|
+
| Cline | `.clinerules/<name>.md` | Markdown per skill |
|
|
34
|
+
| JetBrains AI | `.aiassistant/rules/<name>.md` | Markdown per skill |
|
|
35
|
+
| Amazon Q | `.amazonq/rules/<name>.md` | Markdown per skill |
|
|
36
|
+
| Aider | `CONVENTIONS.md` + `.aider.conf.yml` | Single markdown + YAML |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Why This Exists
|
|
16
41
|
|
|
17
42
|
Modern Android & KMP projects grow complex quickly.
|
|
18
43
|
|
|
@@ -27,7 +52,7 @@ These AI skills:
|
|
|
27
52
|
|
|
28
53
|
---
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+
## Skill Ecosystem
|
|
31
56
|
|
|
32
57
|
```mermaid
|
|
33
58
|
flowchart LR
|
|
@@ -45,19 +70,24 @@ flowchart LR
|
|
|
45
70
|
|
|
46
71
|
E --> J[Shared UI Discipline]
|
|
47
72
|
E --> K[Platform-owned Navigation]
|
|
73
|
+
|
|
74
|
+
L[kotlin-coroutines-best-practices]
|
|
75
|
+
C --- L
|
|
76
|
+
D --- L
|
|
77
|
+
E --- L
|
|
78
|
+
L --> M[Structured Concurrency & Flow]
|
|
48
79
|
```
|
|
49
80
|
|
|
50
81
|
---
|
|
51
82
|
|
|
52
|
-
|
|
83
|
+
## Included Skills
|
|
53
84
|
|
|
54
|
-
|
|
85
|
+
### 1. compose-best-practices
|
|
55
86
|
|
|
56
87
|
For Android-only Jetpack Compose apps.
|
|
57
88
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- Material3-only (🚫 no M2 mixing)
|
|
89
|
+
**Enforces:**
|
|
90
|
+
- Material3-only (no M2 mixing)
|
|
61
91
|
- Stateless composables + UDF
|
|
62
92
|
- StateFlow + SharedFlow patterns
|
|
63
93
|
- Lifecycle-aware collection
|
|
@@ -67,12 +97,11 @@ For Android-only Jetpack Compose apps.
|
|
|
67
97
|
|
|
68
98
|
---
|
|
69
99
|
|
|
70
|
-
|
|
100
|
+
### 2. kmp-architecture-best-practices
|
|
71
101
|
|
|
72
102
|
For shared business logic in Kotlin Multiplatform.
|
|
73
103
|
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
**Enforces:**
|
|
76
105
|
- No Android leakage into commonMain
|
|
77
106
|
- No java.time in shared code
|
|
78
107
|
- Proper expect/actual boundaries
|
|
@@ -82,12 +111,11 @@ For shared business logic in Kotlin Multiplatform.
|
|
|
82
111
|
|
|
83
112
|
---
|
|
84
113
|
|
|
85
|
-
|
|
114
|
+
### 3. compose-multiplatform-best-practices
|
|
86
115
|
|
|
87
116
|
For shared UI in commonMain using Compose Multiplatform.
|
|
88
117
|
|
|
89
|
-
|
|
90
|
-
|
|
118
|
+
**Enforces:**
|
|
91
119
|
- No Android ViewModel in shared UI
|
|
92
120
|
- Platform-owned navigation
|
|
93
121
|
- Shared state holder model
|
|
@@ -96,7 +124,22 @@ For shared UI in commonMain using Compose Multiplatform.
|
|
|
96
124
|
|
|
97
125
|
---
|
|
98
126
|
|
|
99
|
-
|
|
127
|
+
### 4. kotlin-coroutines-best-practices
|
|
128
|
+
|
|
129
|
+
Cross-cutting skill that always activates alongside the project-type-specific skill.
|
|
130
|
+
|
|
131
|
+
**Enforces:**
|
|
132
|
+
- No GlobalScope — scoped coroutines only
|
|
133
|
+
- Structured concurrency with parent-child job hierarchies
|
|
134
|
+
- Cooperative cancellation (isActive, ensureActive)
|
|
135
|
+
- Never catch CancellationException
|
|
136
|
+
- Dispatcher injection — no hardcoded Dispatchers.Main in shared code
|
|
137
|
+
- StateFlow for UI state, SharedFlow for events
|
|
138
|
+
- Test coroutines with TestDispatcher + runTest
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Enterprise Mode (Auto-Detection)
|
|
100
143
|
|
|
101
144
|
Enterprise Mode activates automatically if the repository contains:
|
|
102
145
|
|
|
@@ -130,7 +173,117 @@ flowchart TD
|
|
|
130
173
|
|
|
131
174
|
---
|
|
132
175
|
|
|
133
|
-
|
|
176
|
+
## Install via npx
|
|
177
|
+
|
|
178
|
+
### Global install (default: Codex + Claude Code)
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npx android-ai-skills@latest
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Install only one skill
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npx android-ai-skills@latest --android-only
|
|
188
|
+
npx android-ai-skills@latest --kmp-only
|
|
189
|
+
npx android-ai-skills@latest --compose-mp-only
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Install only for one target
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npx android-ai-skills@latest --target codex
|
|
196
|
+
npx android-ai-skills@latest --target claude
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Dry run
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npx android-ai-skills@latest --dry-run
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Uninstall
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npx android-ai-skills@latest uninstall
|
|
209
|
+
npx android-ai-skills@latest uninstall --target codex
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Project-level init (all 9 tools)
|
|
215
|
+
|
|
216
|
+
Generate project-level instruction files for all supported AI tools:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
npx android-ai-skills@latest init
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Select specific tools
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
npx android-ai-skills@latest init --tools cursor,copilot
|
|
226
|
+
npx android-ai-skills@latest init --tools claude,codex
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Exclude tools
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npx android-ai-skills@latest init --exclude aider
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Smaller output (skip reference docs)
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npx android-ai-skills@latest init --no-references
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Overwrite existing files
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
npx android-ai-skills@latest init --force
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Generated files
|
|
248
|
+
|
|
249
|
+
Running `init` with defaults creates:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
AGENTS.md # Codex
|
|
253
|
+
CLAUDE.md # Claude Code
|
|
254
|
+
.github/copilot-instructions.md # GitHub Copilot
|
|
255
|
+
.cursor/rules/compose-best-practices.mdc # Cursor (per skill)
|
|
256
|
+
.cursor/rules/kmp-architecture-best-practices.mdc
|
|
257
|
+
.cursor/rules/compose-multiplatform-best-practices.mdc
|
|
258
|
+
.cursor/rules/kotlin-coroutines-best-practices.mdc
|
|
259
|
+
.windsurfrules # Windsurf
|
|
260
|
+
.clinerules/compose-best-practices.md # Cline (per skill)
|
|
261
|
+
.clinerules/kmp-architecture-best-practices.md
|
|
262
|
+
.clinerules/compose-multiplatform-best-practices.md
|
|
263
|
+
.clinerules/kotlin-coroutines-best-practices.md
|
|
264
|
+
.aiassistant/rules/compose-best-practices.md # JetBrains AI (per skill)
|
|
265
|
+
.aiassistant/rules/kmp-architecture-best-practices.md
|
|
266
|
+
.aiassistant/rules/compose-multiplatform-best-practices.md
|
|
267
|
+
.aiassistant/rules/kotlin-coroutines-best-practices.md
|
|
268
|
+
.amazonq/rules/compose-best-practices.md # Amazon Q (per skill)
|
|
269
|
+
.amazonq/rules/kmp-architecture-best-practices.md
|
|
270
|
+
.amazonq/rules/compose-multiplatform-best-practices.md
|
|
271
|
+
.amazonq/rules/kotlin-coroutines-best-practices.md
|
|
272
|
+
CONVENTIONS.md # Aider
|
|
273
|
+
.aider.conf.yml
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Print resolved paths
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
npx android-ai-skills@latest print-paths
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Performance Governance
|
|
134
287
|
|
|
135
288
|
Performance is treated as a first-class citizen.
|
|
136
289
|
|
|
@@ -144,7 +297,7 @@ Performance is treated as a first-class citizen.
|
|
|
144
297
|
|
|
145
298
|
---
|
|
146
299
|
|
|
147
|
-
|
|
300
|
+
## Architecture Governance
|
|
148
301
|
|
|
149
302
|
```mermaid
|
|
150
303
|
flowchart TB
|
|
@@ -168,7 +321,7 @@ Principles:
|
|
|
168
321
|
|
|
169
322
|
---
|
|
170
323
|
|
|
171
|
-
|
|
324
|
+
## Stability & Compose Compiler Alignment
|
|
172
325
|
|
|
173
326
|
The skills encourage:
|
|
174
327
|
|
|
@@ -182,112 +335,18 @@ This ensures Compose can skip recomposition effectively.
|
|
|
182
335
|
|
|
183
336
|
---
|
|
184
337
|
|
|
185
|
-
|
|
338
|
+
## Repository Structure
|
|
186
339
|
|
|
187
340
|
```
|
|
188
341
|
compose-best-practices/
|
|
189
342
|
kmp-architecture-best-practices/
|
|
190
343
|
compose-multiplatform-best-practices/
|
|
344
|
+
kotlin-coroutines-best-practices/
|
|
191
345
|
README.md
|
|
192
346
|
```
|
|
193
347
|
|
|
194
348
|
---
|
|
195
349
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
1. Place the skills inside:
|
|
199
|
-
- ~/.codex/skills/
|
|
200
|
-
- ~/.claude/skills/
|
|
201
|
-
- or project-level .codex/skills/
|
|
202
|
-
|
|
203
|
-
2. Add AGENTS.md to your project root (optional but recommended).
|
|
204
|
-
|
|
205
|
-
3. The correct skill activates automatically based on project structure.
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
209
|
-
# 🎉 Result
|
|
210
|
-
|
|
211
|
-
You now have:
|
|
212
|
-
|
|
213
|
-
- Predictable AI code reviews
|
|
214
|
-
- Performance-aware AI refactors
|
|
215
|
-
- Architecture enforcement across Android + KMP
|
|
216
|
-
- Enterprise-ready governance without overengineering
|
|
350
|
+
## License
|
|
217
351
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
# 🛡 License
|
|
221
|
-
|
|
222
|
-
MIT – Use freely in personal, startup, or enterprise projects.
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
# 🧰 Install via npx
|
|
226
|
-
|
|
227
|
-
You can install the skills globally into the standard locations for **Codex** and **Claude**:
|
|
228
|
-
|
|
229
|
-
- `~/.codex/skills/`
|
|
230
|
-
- `~/.claude/skills/`
|
|
231
|
-
|
|
232
|
-
## Install (default: both)
|
|
233
|
-
|
|
234
|
-
```bash
|
|
235
|
-
npx android-ai-skills@latest
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
## Install only one skill
|
|
239
|
-
|
|
240
|
-
```bash
|
|
241
|
-
npx android-ai-skills@latest --android-only
|
|
242
|
-
npx android-ai-skills@latest --kmp-only
|
|
243
|
-
npx android-ai-skills@latest --compose-mp-only
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
## Install only for one target
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
npx android-ai-skills@latest --target codex
|
|
250
|
-
npx android-ai-skills@latest --target claude
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
## Dry run
|
|
254
|
-
|
|
255
|
-
```bash
|
|
256
|
-
npx android-ai-skills@latest --dry-run
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
## Uninstall
|
|
260
|
-
|
|
261
|
-
```bash
|
|
262
|
-
npx android-ai-skills@latest uninstall
|
|
263
|
-
npx android-ai-skills@latest uninstall --target codex
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
# 🧾 Auto-detect Enterprise Mode (Tooling-aware)
|
|
269
|
-
|
|
270
|
-
Enterprise Mode activates automatically **only if tooling is detected** in the project root:
|
|
271
|
-
|
|
272
|
-
- `detekt.yml` / `detekt.yaml`
|
|
273
|
-
- `lint.xml`
|
|
274
|
-
- ktlint config
|
|
275
|
-
- spotless config
|
|
276
|
-
|
|
277
|
-
If tooling is not detected, the skills remain tool-agnostic and **do not** enforce lint/detekt specifics.
|
|
278
|
-
|
|
279
|
-
### Generate an AGENTS.md in your repo
|
|
280
|
-
|
|
281
|
-
```bash
|
|
282
|
-
npx android-ai-skills@latest init
|
|
283
|
-
# or write it somewhere else:
|
|
284
|
-
npx android-ai-skills@latest init --path .
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
### Print where skills will be installed
|
|
288
|
-
|
|
289
|
-
```bash
|
|
290
|
-
npx android-ai-skills@latest print-paths
|
|
291
|
-
# or include paths during install:
|
|
292
|
-
npx android-ai-skills@latest --print-paths
|
|
293
|
-
```
|
|
352
|
+
MIT -- Use freely in personal, startup, or enterprise projects.
|
package/bin/android-ai-skills.js
CHANGED
|
@@ -11,46 +11,169 @@ const ALL_SKILLS = [
|
|
|
11
11
|
"compose-best-practices",
|
|
12
12
|
"kmp-architecture-best-practices",
|
|
13
13
|
"compose-multiplatform-best-practices",
|
|
14
|
+
"kotlin-coroutines-best-practices",
|
|
14
15
|
];
|
|
15
16
|
|
|
17
|
+
// ── Tool registry for project-level init ──────────────────────────────
|
|
18
|
+
|
|
19
|
+
const INIT_TOOLS = {
|
|
20
|
+
codex: { file: "AGENTS.md", type: "single" },
|
|
21
|
+
claude: { file: "CLAUDE.md", type: "single" },
|
|
22
|
+
copilot: { dir: ".github", file: "copilot-instructions.md", type: "single" },
|
|
23
|
+
cursor: { dir: ".cursor/rules", type: "per-skill", ext: ".mdc" },
|
|
24
|
+
windsurf: { file: ".windsurfrules", type: "single" },
|
|
25
|
+
cline: { dir: ".clinerules", type: "per-skill", ext: ".md" },
|
|
26
|
+
jetbrains: { dir: ".aiassistant/rules", type: "per-skill", ext: ".md" },
|
|
27
|
+
amazonq: { dir: ".amazonq/rules", type: "per-skill", ext: ".md" },
|
|
28
|
+
aider: { file: "CONVENTIONS.md", type: "single", extra: ".aider.conf.yml" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const INIT_TOOL_NAMES = Object.keys(INIT_TOOLS);
|
|
32
|
+
|
|
33
|
+
// ── Content pipeline ──────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
function parseYamlFrontmatter(text) {
|
|
36
|
+
const match = text.match(/^[\s]*---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
37
|
+
if (!match) return { meta: {}, body: text.trim() };
|
|
38
|
+
const raw = match[1];
|
|
39
|
+
const meta = {};
|
|
40
|
+
for (const line of raw.split("\n")) {
|
|
41
|
+
const idx = line.indexOf(":");
|
|
42
|
+
if (idx > 0) meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
43
|
+
}
|
|
44
|
+
return { meta, body: match[2].trim() };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readSkillContent(skillName) {
|
|
48
|
+
const skillDir = path.join(projectRoot, skillName);
|
|
49
|
+
const skillMd = fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf8");
|
|
50
|
+
const { meta, body } = parseYamlFrontmatter(skillMd);
|
|
51
|
+
|
|
52
|
+
const references = [];
|
|
53
|
+
const refsDir = path.join(skillDir, "references");
|
|
54
|
+
if (fs.existsSync(refsDir)) {
|
|
55
|
+
for (const f of fs.readdirSync(refsDir).sort()) {
|
|
56
|
+
if (!f.endsWith(".md")) continue;
|
|
57
|
+
references.push({
|
|
58
|
+
name: f.replace(/\.md$/, ""),
|
|
59
|
+
content: fs.readFileSync(path.join(refsDir, f), "utf8").trim(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { name: skillName, meta, body, references };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function activationMatrix() {
|
|
68
|
+
return `## Skill activation matrix
|
|
69
|
+
|
|
70
|
+
If the project contains \`commonMain\` (KMP):
|
|
71
|
+
|
|
72
|
+
- If shared UI composables exist in \`commonMain\`:
|
|
73
|
+
- Use **compose-multiplatform-best-practices**
|
|
74
|
+
- Else (shared business logic only):
|
|
75
|
+
- Use **kmp-architecture-best-practices**
|
|
76
|
+
|
|
77
|
+
If the project is Android-only:
|
|
78
|
+
|
|
79
|
+
- Use **compose-best-practices**
|
|
80
|
+
|
|
81
|
+
**Always activate:** **kotlin-coroutines-best-practices** (cross-cutting concern — applies to all project types).
|
|
82
|
+
|
|
83
|
+
## Enterprise Mode auto-detection (optional)
|
|
84
|
+
|
|
85
|
+
Enterprise Mode turns on automatically when tooling is detected in the repo root:
|
|
86
|
+
- detekt.yml / detekt.yaml
|
|
87
|
+
- lint.xml
|
|
88
|
+
- ktlint/spotless config
|
|
89
|
+
|
|
90
|
+
If not detected, tool-specific enforcement is disabled (tool-agnostic guidance only).`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function generateMarkdown(skill, { includeRefs = true } = {}) {
|
|
94
|
+
let out = skill.body;
|
|
95
|
+
if (includeRefs && skill.references.length) {
|
|
96
|
+
for (const ref of skill.references) {
|
|
97
|
+
out += `\n\n---\n\n${ref.content}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out + "\n";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function generateMdc(skill, { includeRefs = true } = {}) {
|
|
104
|
+
const desc = skill.meta.description || skill.name;
|
|
105
|
+
const body = generateMarkdown(skill, { includeRefs });
|
|
106
|
+
return `---
|
|
107
|
+
description: ${desc}
|
|
108
|
+
globs: "**/*.kt,**/*.kts"
|
|
109
|
+
alwaysApply: false
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
${body}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function flattenSkillForClaude(skill) {
|
|
116
|
+
return generateMarkdown(skill, { includeRefs: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function generateSingleFile(skills, { includeRefs = true } = {}) {
|
|
120
|
+
let out = `# Android AI Skills\n\n${activationMatrix()}\n`;
|
|
121
|
+
for (const skill of skills) {
|
|
122
|
+
out += `\n---\n\n${generateMarkdown(skill, { includeRefs })}`;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── CLI helpers ───────────────────────────────────────────────────────
|
|
128
|
+
|
|
16
129
|
function help() {
|
|
17
130
|
console.log(`
|
|
18
|
-
android-ai-skills — install
|
|
131
|
+
android-ai-skills — install AI governance skills for Android, KMP & Compose
|
|
132
|
+
|
|
133
|
+
Supported tools: Codex, Claude Code, GitHub Copilot, Cursor, Windsurf,
|
|
134
|
+
Cline, JetBrains AI, Amazon Q, Aider.
|
|
19
135
|
|
|
20
136
|
Usage:
|
|
21
|
-
npx android-ai-skills@latest [options]
|
|
22
|
-
npx android-ai-skills@latest uninstall [options]
|
|
23
|
-
npx android-ai-skills@latest init [options]
|
|
137
|
+
npx android-ai-skills@latest [options] Global install (Codex + Claude)
|
|
138
|
+
npx android-ai-skills@latest uninstall [options] Remove global install
|
|
139
|
+
npx android-ai-skills@latest init [options] Project-level files (all 9 tools)
|
|
24
140
|
npx android-ai-skills@latest print-paths [options]
|
|
25
141
|
|
|
26
|
-
|
|
142
|
+
Skill selection:
|
|
27
143
|
--android-only Install only compose-best-practices
|
|
28
144
|
--kmp-only Install only kmp-architecture-best-practices
|
|
29
145
|
--compose-mp-only Install only compose-multiplatform-best-practices
|
|
30
146
|
|
|
31
|
-
|
|
32
|
-
--target <
|
|
147
|
+
Global install targets:
|
|
148
|
+
--target <all|codex|claude> Default: all
|
|
33
149
|
--codex-only Same as --target codex
|
|
34
150
|
--claude-only Same as --target claude
|
|
35
151
|
|
|
152
|
+
Init options:
|
|
153
|
+
--path <dir> Where to write files (default: current directory)
|
|
154
|
+
--tools <tool,...> Only generate for these tools (comma-separated)
|
|
155
|
+
--exclude <tool,...> Skip these tools (comma-separated)
|
|
156
|
+
--no-references Omit reference docs for smaller output
|
|
157
|
+
|
|
158
|
+
Available tool names: ${INIT_TOOL_NAMES.join(", ")}
|
|
159
|
+
|
|
36
160
|
Advanced:
|
|
37
161
|
--dest <path> Override destination base folder (advanced)
|
|
38
162
|
--dry-run Print actions without writing files
|
|
39
|
-
--force Overwrite existing
|
|
40
|
-
--print-paths Print resolved install paths and exit
|
|
163
|
+
--force Overwrite existing files
|
|
164
|
+
--print-paths Print resolved install paths and exit
|
|
41
165
|
--help Show this help
|
|
42
166
|
|
|
43
|
-
init options:
|
|
44
|
-
--path <dir> Where to write AGENTS.md (default: current directory)
|
|
45
|
-
--force Overwrite existing AGENTS.md
|
|
46
|
-
|
|
47
167
|
Examples:
|
|
48
168
|
npx android-ai-skills@latest
|
|
49
169
|
npx android-ai-skills@latest --android-only
|
|
50
170
|
npx android-ai-skills@latest --target codex
|
|
51
171
|
npx android-ai-skills@latest --dry-run
|
|
52
|
-
npx android-ai-skills@latest uninstall --target
|
|
172
|
+
npx android-ai-skills@latest uninstall --target all
|
|
53
173
|
npx android-ai-skills@latest init
|
|
174
|
+
npx android-ai-skills@latest init --tools cursor,copilot
|
|
175
|
+
npx android-ai-skills@latest init --exclude aider --force
|
|
176
|
+
npx android-ai-skills@latest init --no-references
|
|
54
177
|
npx android-ai-skills@latest print-paths
|
|
55
178
|
`);
|
|
56
179
|
}
|
|
@@ -62,7 +185,12 @@ function parse(argv) {
|
|
|
62
185
|
if (!s.startsWith("--")) { a._.push(s); continue; }
|
|
63
186
|
const k = s.slice(2);
|
|
64
187
|
const n = argv[i + 1];
|
|
65
|
-
const booleanKeys = new Set([
|
|
188
|
+
const booleanKeys = new Set([
|
|
189
|
+
"android-only", "kmp-only", "compose-mp-only",
|
|
190
|
+
"codex-only", "claude-only",
|
|
191
|
+
"dry-run", "force", "help", "print-paths",
|
|
192
|
+
"no-references",
|
|
193
|
+
]);
|
|
66
194
|
if (!booleanKeys.has(k) && n && !n.startsWith("--")) { a[k] = n; i++; }
|
|
67
195
|
else a[k] = true;
|
|
68
196
|
}
|
|
@@ -91,6 +219,12 @@ function removeDir(dest, { dryRun }) {
|
|
|
91
219
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
92
220
|
}
|
|
93
221
|
|
|
222
|
+
function removeFile(dest, { dryRun }) {
|
|
223
|
+
if (!fs.existsSync(dest)) return;
|
|
224
|
+
if (dryRun) return;
|
|
225
|
+
fs.unlinkSync(dest);
|
|
226
|
+
}
|
|
227
|
+
|
|
94
228
|
function resolveSkills(args) {
|
|
95
229
|
const selected = new Set();
|
|
96
230
|
if (args["android-only"] || args["kmp-only"] || args["compose-mp-only"]) {
|
|
@@ -102,63 +236,146 @@ function resolveSkills(args) {
|
|
|
102
236
|
return [...ALL_SKILLS];
|
|
103
237
|
}
|
|
104
238
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
239
|
+
// ── Global install targets ────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
function resolveGlobalTargets(args) {
|
|
242
|
+
let target = (args.target || "all").toLowerCase();
|
|
108
243
|
if (args["codex-only"]) target = "codex";
|
|
109
244
|
if (args["claude-only"]) target = "claude";
|
|
245
|
+
if (target === "both") target = "all";
|
|
110
246
|
|
|
111
|
-
if (args.dest)
|
|
247
|
+
if (args.dest) {
|
|
248
|
+
return [{ type: "codex", base: path.resolve(process.cwd(), args.dest) }];
|
|
249
|
+
}
|
|
112
250
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (target === "
|
|
116
|
-
|
|
251
|
+
const home = os.homedir();
|
|
252
|
+
const targets = [];
|
|
253
|
+
if (target === "all" || target === "codex") {
|
|
254
|
+
targets.push({ type: "codex", base: path.join(home, ".codex", "skills") });
|
|
255
|
+
}
|
|
256
|
+
if (target === "all" || target === "claude") {
|
|
257
|
+
targets.push({ type: "claude", base: path.join(home, ".claude", "rules") });
|
|
258
|
+
}
|
|
259
|
+
if (!targets.length) throw new Error(`Unknown --target ${args.target}. Use all|codex|claude.`);
|
|
260
|
+
return targets;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function checkClaudeMigration(skills) {
|
|
264
|
+
const oldBase = path.join(os.homedir(), ".claude", "skills");
|
|
265
|
+
if (!fs.existsSync(oldBase)) return;
|
|
266
|
+
const found = skills.filter(s => fs.existsSync(path.join(oldBase, s)));
|
|
267
|
+
if (found.length) {
|
|
268
|
+
console.log(`⚠️ Found old Claude skills in ~/.claude/skills/`);
|
|
269
|
+
console.log(` Claude Code reads from ~/.claude/rules/ instead.`);
|
|
270
|
+
console.log(` You can remove the old directory: rm -rf ~/.claude/skills/`);
|
|
271
|
+
console.log("");
|
|
272
|
+
}
|
|
117
273
|
}
|
|
118
274
|
|
|
119
275
|
function printResolvedPaths(args) {
|
|
120
276
|
const skills = resolveSkills(args);
|
|
121
|
-
const targets =
|
|
277
|
+
const targets = resolveGlobalTargets(args);
|
|
122
278
|
console.log("Resolved install paths:");
|
|
123
279
|
for (const t of targets) {
|
|
124
|
-
console.log(`- ${t}`);
|
|
125
|
-
for (const s of skills)
|
|
280
|
+
console.log(`- ${t.base} (${t.type})`);
|
|
281
|
+
for (const s of skills) {
|
|
282
|
+
if (t.type === "claude") {
|
|
283
|
+
console.log(` - ${path.join(t.base, s + ".md")}`);
|
|
284
|
+
} else {
|
|
285
|
+
console.log(` - ${path.join(t.base, s)}/`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
126
288
|
}
|
|
127
289
|
}
|
|
128
290
|
|
|
129
|
-
|
|
130
|
-
const p = path.join(outDir, "AGENTS.md");
|
|
131
|
-
if (fs.existsSync(p) && !force) {
|
|
132
|
-
throw new Error(`AGENTS.md already exists at ${p}. Use --force to overwrite.`);
|
|
133
|
-
}
|
|
134
|
-
const content = `# AGENTS.md
|
|
291
|
+
// ── Init command ──────────────────────────────────────────────────────
|
|
135
292
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
293
|
+
function resolveInitTargets(args) {
|
|
294
|
+
let tools = [...INIT_TOOL_NAMES];
|
|
295
|
+
if (args.tools) {
|
|
296
|
+
tools = args.tools.split(",").map(t => t.trim().toLowerCase());
|
|
297
|
+
for (const t of tools) {
|
|
298
|
+
if (!INIT_TOOLS[t]) throw new Error(`Unknown tool: ${t}. Available: ${INIT_TOOL_NAMES.join(", ")}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (args.exclude) {
|
|
302
|
+
const excl = new Set(args.exclude.split(",").map(t => t.trim().toLowerCase()));
|
|
303
|
+
tools = tools.filter(t => !excl.has(t));
|
|
304
|
+
}
|
|
305
|
+
return tools;
|
|
306
|
+
}
|
|
139
307
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
- Use **kmp-architecture-best-practices**
|
|
308
|
+
function writeProjectFiles(outDir, skills, tools, { dryRun, force, includeRefs }) {
|
|
309
|
+
const skillContents = skills.map(s => readSkillContent(s));
|
|
310
|
+
const written = [];
|
|
144
311
|
|
|
145
|
-
|
|
312
|
+
for (const toolName of tools) {
|
|
313
|
+
const cfg = INIT_TOOLS[toolName];
|
|
146
314
|
|
|
147
|
-
|
|
315
|
+
if (cfg.type === "single") {
|
|
316
|
+
const filePath = cfg.dir
|
|
317
|
+
? path.join(outDir, cfg.dir, cfg.file)
|
|
318
|
+
: path.join(outDir, cfg.file);
|
|
148
319
|
|
|
149
|
-
|
|
320
|
+
if (fs.existsSync(filePath) && !force) {
|
|
321
|
+
console.log(`⏭️ Skipped ${path.relative(outDir, filePath)} (exists, use --force)`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
150
324
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
325
|
+
const content = generateSingleFile(skillContents, { includeRefs });
|
|
326
|
+
if (!dryRun) {
|
|
327
|
+
ensureDir(path.dirname(filePath), false);
|
|
328
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
329
|
+
}
|
|
330
|
+
console.log(`${dryRun ? "🧪" : "✅"} ${toolName} → ${path.relative(outDir, filePath)}`);
|
|
331
|
+
written.push(filePath);
|
|
332
|
+
|
|
333
|
+
// Aider extra: .aider.conf.yml
|
|
334
|
+
if (cfg.extra) {
|
|
335
|
+
const extraPath = path.join(outDir, cfg.extra);
|
|
336
|
+
if (!fs.existsSync(extraPath)) {
|
|
337
|
+
const yaml = `read:\n - ${cfg.file}\n`;
|
|
338
|
+
if (!dryRun) fs.writeFileSync(extraPath, yaml, "utf8");
|
|
339
|
+
console.log(`${dryRun ? "🧪" : "✅"} ${toolName} → ${path.relative(outDir, extraPath)}`);
|
|
340
|
+
written.push(extraPath);
|
|
341
|
+
} else {
|
|
342
|
+
console.log(`⏭️ Skipped ${path.relative(outDir, extraPath)} (exists)`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
// per-skill
|
|
347
|
+
const dir = path.join(outDir, cfg.dir);
|
|
348
|
+
for (const skill of skillContents) {
|
|
349
|
+
const fileName = skill.name + cfg.ext;
|
|
350
|
+
const filePath = path.join(dir, fileName);
|
|
351
|
+
|
|
352
|
+
if (fs.existsSync(filePath) && !force) {
|
|
353
|
+
console.log(`⏭️ Skipped ${path.relative(outDir, filePath)} (exists, use --force)`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let content;
|
|
358
|
+
if (toolName === "cursor") {
|
|
359
|
+
content = generateMdc(skill, { includeRefs });
|
|
360
|
+
} else {
|
|
361
|
+
content = generateMarkdown(skill, { includeRefs });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!dryRun) {
|
|
365
|
+
ensureDir(dir, false);
|
|
366
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
367
|
+
}
|
|
368
|
+
console.log(`${dryRun ? "🧪" : "✅"} ${toolName} → ${path.relative(outDir, filePath)}`);
|
|
369
|
+
written.push(filePath);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
155
373
|
|
|
156
|
-
|
|
157
|
-
`;
|
|
158
|
-
fs.writeFileSync(p, content, "utf8");
|
|
159
|
-
return p;
|
|
374
|
+
return written;
|
|
160
375
|
}
|
|
161
376
|
|
|
377
|
+
// ── Main ──────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
162
379
|
function main() {
|
|
163
380
|
const args = parse(process.argv.slice(2));
|
|
164
381
|
const sub = (args._[0] || "").toLowerCase();
|
|
@@ -172,9 +389,20 @@ function main() {
|
|
|
172
389
|
|
|
173
390
|
if (sub === "init") {
|
|
174
391
|
const outDir = path.resolve(process.cwd(), args.path || ".");
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
392
|
+
const dryRun = !!args["dry-run"];
|
|
393
|
+
const force = !!args.force;
|
|
394
|
+
const includeRefs = !args["no-references"];
|
|
395
|
+
const skills = resolveSkills(args);
|
|
396
|
+
const tools = resolveInitTargets(args);
|
|
397
|
+
|
|
398
|
+
console.log(`🚀 Initializing project-level AI skill files`);
|
|
399
|
+
if (dryRun) console.log("🧪 Dry-run (no files will be written)");
|
|
400
|
+
console.log(` Tools: ${tools.join(", ")}`);
|
|
401
|
+
console.log(` Skills: ${skills.join(", ")}`);
|
|
402
|
+
console.log("");
|
|
403
|
+
|
|
404
|
+
writeProjectFiles(outDir, skills, tools, { dryRun, force, includeRefs });
|
|
405
|
+
console.log("\n🎉 Done.");
|
|
178
406
|
return;
|
|
179
407
|
}
|
|
180
408
|
|
|
@@ -184,27 +412,50 @@ function main() {
|
|
|
184
412
|
|
|
185
413
|
if (args["print-paths"]) {
|
|
186
414
|
printResolvedPaths(args);
|
|
187
|
-
|
|
415
|
+
console.log("");
|
|
188
416
|
}
|
|
189
417
|
|
|
190
418
|
const skills = resolveSkills(args);
|
|
191
|
-
const targets =
|
|
419
|
+
const targets = resolveGlobalTargets(args);
|
|
420
|
+
|
|
421
|
+
if (!uninstall) checkClaudeMigration(skills);
|
|
192
422
|
|
|
193
423
|
console.log(`🚀 ${uninstall ? "Uninstalling" : "Installing"} android-ai-skills`);
|
|
194
424
|
if (dryRun) console.log("🧪 Dry-run (no files will be written)");
|
|
195
425
|
console.log("");
|
|
196
426
|
|
|
197
|
-
for (const
|
|
198
|
-
ensureDir(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
427
|
+
for (const target of targets) {
|
|
428
|
+
ensureDir(target.base, dryRun);
|
|
429
|
+
|
|
430
|
+
if (target.type === "codex") {
|
|
431
|
+
for (const skill of skills) {
|
|
432
|
+
const src = path.join(projectRoot, skill);
|
|
433
|
+
const dest = path.join(target.base, skill);
|
|
434
|
+
if (uninstall) {
|
|
435
|
+
console.log(`🗑️ ${skill} → ${dest}`);
|
|
436
|
+
removeDir(dest, { dryRun });
|
|
437
|
+
} else {
|
|
438
|
+
console.log(`✅ ${skill} → ${dest}`);
|
|
439
|
+
copyDir(src, dest, { dryRun, force });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} else if (target.type === "claude") {
|
|
443
|
+
for (const skill of skills) {
|
|
444
|
+
const dest = path.join(target.base, skill + ".md");
|
|
445
|
+
if (uninstall) {
|
|
446
|
+
console.log(`🗑️ ${skill} → ${dest}`);
|
|
447
|
+
removeFile(dest, { dryRun });
|
|
448
|
+
} else {
|
|
449
|
+
if (fs.existsSync(dest) && !force) {
|
|
450
|
+
throw new Error(`Destination exists: ${dest}. Use --force.`);
|
|
451
|
+
}
|
|
452
|
+
const content = flattenSkillForClaude(readSkillContent(skill));
|
|
453
|
+
if (!dryRun) {
|
|
454
|
+
ensureDir(target.base, false);
|
|
455
|
+
fs.writeFileSync(dest, content, "utf8");
|
|
456
|
+
}
|
|
457
|
+
console.log(`✅ ${skill} → ${dest}`);
|
|
458
|
+
}
|
|
208
459
|
}
|
|
209
460
|
}
|
|
210
461
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
name: kotlin-coroutines-best-practices
|
|
4
|
+
description: Kotlin Coroutines & Flow best practices for Android and KMP projects.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Kotlin Coroutines & Flow Best Practices
|
|
8
|
+
|
|
9
|
+
Cross-cutting skill — always activates alongside the project-type-specific skill.
|
|
10
|
+
|
|
11
|
+
## Hard Rules
|
|
12
|
+
- No GlobalScope — use scoped coroutines (viewModelScope, lifecycleScope, custom CoroutineScope).
|
|
13
|
+
- No blocking calls on Dispatchers.Main.
|
|
14
|
+
- Structured concurrency — maintain parent-child job hierarchies.
|
|
15
|
+
- Cooperative cancellation — check isActive in long-running loops.
|
|
16
|
+
- Never catch CancellationException.
|
|
17
|
+
- Never swallow exceptions silently.
|
|
18
|
+
- Inject dispatchers — no hardcoded Dispatchers.Main in shared code.
|
|
19
|
+
- Prefer Flow over callbacks.
|
|
20
|
+
- Test coroutines with TestDispatcher + runTest.
|
|
21
|
+
|
|
22
|
+
## Core Patterns
|
|
23
|
+
- StateFlow for UI state, SharedFlow for events.
|
|
24
|
+
- stateIn with WhileSubscribed(5_000) for cold-to-hot conversion.
|
|
25
|
+
- withContext for dispatcher switching.
|
|
26
|
+
- supervisorScope for isolated failure handling.
|
|
27
|
+
- flowOn to change upstream dispatcher.
|
|
28
|
+
- catch operator for upstream flow errors.
|
|
29
|
+
- Prefer callbackFlow over raw Channel for callback interop.
|
|
30
|
+
|
|
31
|
+
## References
|
|
32
|
+
- references/structured_concurrency.md
|
|
33
|
+
- references/error_handling.md
|
|
34
|
+
- references/testing.md
|
|
35
|
+
- references/dispatchers.md
|
|
36
|
+
- references/flow_types.md
|
|
37
|
+
- references/flow_operators.md
|
|
38
|
+
- references/channels.md
|
|
39
|
+
- references/anti_patterns.md
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Coroutine Anti-Patterns
|
|
2
|
+
|
|
3
|
+
## Scope & Lifecycle
|
|
4
|
+
- GlobalScope: leaks coroutines — no structured cancellation. Use scoped coroutines.
|
|
5
|
+
- launch without a scope: fire-and-forget with no lifecycle management.
|
|
6
|
+
- Ignoring the Job returned by launch: cannot cancel or track completion.
|
|
7
|
+
|
|
8
|
+
## Cancellation
|
|
9
|
+
- Catching CancellationException: breaks cancellation propagation. If catching Exception, rethrow CancellationException.
|
|
10
|
+
- Infinite while(true) without isActive check: coroutine never cooperatively cancels.
|
|
11
|
+
- runBlocking in production Android code: blocks the calling thread, risk of ANR on Main.
|
|
12
|
+
|
|
13
|
+
## Threading
|
|
14
|
+
- Blocking calls on Dispatchers.Main: Thread.sleep, synchronous I/O, heavy computation — causes ANR.
|
|
15
|
+
- Hardcoded Dispatchers.Main in commonMain (KMP): Main is Android-specific, crashes on other platforms.
|
|
16
|
+
|
|
17
|
+
## Flows
|
|
18
|
+
- Collecting flows in composable body (outside LaunchedEffect): triggers new collection on every recomposition.
|
|
19
|
+
- SharingStarted.Eagerly without justification: wastes resources when no collectors are active.
|
|
20
|
+
- withContext inside flow {} builder: use flowOn instead.
|
|
21
|
+
|
|
22
|
+
## Error Handling
|
|
23
|
+
- Swallowing exceptions silently: catch {} with no logging or propagation hides bugs.
|
|
24
|
+
- CoroutineExceptionHandler on child coroutine: ignored — install on root scope only.
|
|
25
|
+
- runCatching on suspend functions without rethrowing CancellationException.
|
|
26
|
+
|
|
27
|
+
## Over-engineering
|
|
28
|
+
- Mutex when limitedParallelism(1) suffices.
|
|
29
|
+
- Raw Channel when SharedFlow or callbackFlow fits.
|
|
30
|
+
- Custom CoroutineScope when viewModelScope/lifecycleScope already matches lifecycle.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Channels
|
|
2
|
+
|
|
3
|
+
- Hot communication primitive — elements are consumed once (not broadcast).
|
|
4
|
+
- Prefer Flow for most use cases — channels are lower-level.
|
|
5
|
+
|
|
6
|
+
## Channel Types
|
|
7
|
+
- Channel.UNLIMITED: unlimited buffer, sender never suspends.
|
|
8
|
+
- Channel.BUFFERED: default buffer size (64), sender suspends when full.
|
|
9
|
+
- Channel.RENDEZVOUS: zero buffer, sender suspends until receiver is ready.
|
|
10
|
+
- Channel.CONFLATED: buffer of 1, new values overwrite unread value.
|
|
11
|
+
|
|
12
|
+
## Patterns
|
|
13
|
+
- produce {} builder: returns ReceiveChannel, auto-closes on completion.
|
|
14
|
+
- Fan-out: multiple coroutines receiving from one channel (load balancing).
|
|
15
|
+
- Fan-in: multiple coroutines sending to one channel (aggregation).
|
|
16
|
+
- close() when done — signals no more elements.
|
|
17
|
+
- consumeEach {} for safe consumption with automatic cancellation.
|
|
18
|
+
|
|
19
|
+
## When to Use
|
|
20
|
+
- Prefer callbackFlow over raw Channel for bridging callback APIs.
|
|
21
|
+
- Use Channel for worker pool / fan-out patterns.
|
|
22
|
+
- Use Channel for single-consumer event queues when SharedFlow doesn't fit.
|
|
23
|
+
- Avoid Channel for state — use StateFlow instead.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Dispatchers
|
|
2
|
+
|
|
3
|
+
## Built-in Dispatchers
|
|
4
|
+
- Dispatchers.Main: UI thread only — use for UI updates, state emissions.
|
|
5
|
+
- Dispatchers.IO: blocking I/O (network, disk, database) — backed by ~64 threads.
|
|
6
|
+
- Dispatchers.Default: CPU-intensive work (sorting, parsing, serialization) — thread count = CPU cores.
|
|
7
|
+
- Dispatchers.Unconfined: avoid — runs in caller's thread until first suspension, then resumes in whatever thread.
|
|
8
|
+
|
|
9
|
+
## Switching
|
|
10
|
+
- withContext(Dispatchers.IO) { ... } — switch dispatcher for a block, return result.
|
|
11
|
+
- Never nest withContext with the same dispatcher — no-op overhead.
|
|
12
|
+
- flowOn(Dispatchers.IO) — changes upstream flow dispatcher.
|
|
13
|
+
|
|
14
|
+
## Injection
|
|
15
|
+
- Accept CoroutineDispatcher as constructor parameter.
|
|
16
|
+
- Provide via DI (Hilt @Qualifier annotations: @IoDispatcher, @DefaultDispatcher, @MainDispatcher).
|
|
17
|
+
- Never hardcode Dispatchers.Main in commonMain (KMP) — Main is Android-specific.
|
|
18
|
+
- In KMP shared code, inject all dispatchers.
|
|
19
|
+
|
|
20
|
+
## Advanced
|
|
21
|
+
- limitedParallelism(n): create a view limiting concurrent coroutines.
|
|
22
|
+
- Use limitedParallelism(1) for single-threaded access (replaces Mutex in some cases).
|
|
23
|
+
- IO.limitedParallelism(4) for rate-limiting external API calls.
|
|
24
|
+
- Default.limitedParallelism(2) for CPU-bound tasks that shouldn't starve others.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
## Suspend Functions
|
|
4
|
+
- Use try-catch inside suspend functions for recoverable errors.
|
|
5
|
+
- Return Result<T> or sealed class for expected failures (network, parsing).
|
|
6
|
+
- runCatching {} wraps in Result<T> — but catches CancellationException; re-throw it.
|
|
7
|
+
|
|
8
|
+
## Coroutine Scopes
|
|
9
|
+
- CoroutineExceptionHandler: install on root scope only (launch, not async).
|
|
10
|
+
- Child exceptions propagate to parent — handler on child is ignored.
|
|
11
|
+
- supervisorScope: isolates child failures — one child crash doesn't cancel siblings.
|
|
12
|
+
- Use supervisorScope when launching independent parallel work.
|
|
13
|
+
|
|
14
|
+
## Flows
|
|
15
|
+
- catch {} operator: catches upstream errors only.
|
|
16
|
+
- Place catch before terminal operators (collect, stateIn).
|
|
17
|
+
- Use onCompletion {} to detect both success and failure.
|
|
18
|
+
- retryWhen { cause, attempt -> } for transient failures with backoff.
|
|
19
|
+
- Exponential backoff: delay(minOf(2.0.pow(attempt).toLong() * 1000, 30_000)).
|
|
20
|
+
|
|
21
|
+
## Anti-patterns
|
|
22
|
+
- Never swallow exceptions silently — at minimum log them.
|
|
23
|
+
- Never catch CancellationException — breaks structured concurrency.
|
|
24
|
+
- Don't use CoroutineExceptionHandler as a substitute for proper error handling.
|
|
25
|
+
- Avoid runCatching on suspend functions without re-throwing CancellationException.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Flow Operators
|
|
2
|
+
|
|
3
|
+
## Transform
|
|
4
|
+
- map { } — transform each element.
|
|
5
|
+
- filter { } — keep elements matching predicate.
|
|
6
|
+
- mapNotNull { } — map + filter nulls in one step.
|
|
7
|
+
- transform { } — emit zero or more values per upstream element.
|
|
8
|
+
|
|
9
|
+
## Flattening
|
|
10
|
+
- flatMapLatest { } — cancel previous inner flow when new value arrives (use for search-as-you-type).
|
|
11
|
+
- flatMapConcat { } — process inner flows sequentially.
|
|
12
|
+
- flatMapMerge { } — process inner flows concurrently (configurable concurrency).
|
|
13
|
+
|
|
14
|
+
## Combining
|
|
15
|
+
- combine(flow1, flow2) { a, b -> } — emit on any change, using latest from each.
|
|
16
|
+
- zip(flow1, flow2) { a, b -> } — 1:1 pairing, completes when either flow completes.
|
|
17
|
+
- merge(flow1, flow2) — interleave emissions from multiple flows.
|
|
18
|
+
|
|
19
|
+
## Rate Limiting
|
|
20
|
+
- debounce(timeoutMillis) — emit after silence period (search input).
|
|
21
|
+
- sample(periodMillis) — emit latest value at fixed intervals.
|
|
22
|
+
- distinctUntilChanged() — skip consecutive duplicates.
|
|
23
|
+
|
|
24
|
+
## Buffering
|
|
25
|
+
- buffer() — run collector and emitter concurrently (back-pressure relief).
|
|
26
|
+
- conflate() — skip intermediate values when collector is slow.
|
|
27
|
+
- buffer(Channel.CONFLATED) — equivalent to conflate().
|
|
28
|
+
|
|
29
|
+
## Error Handling
|
|
30
|
+
- catch { } — catch upstream exceptions, can emit fallback values.
|
|
31
|
+
- retryWhen { cause, attempt -> } — retry upstream on transient failures.
|
|
32
|
+
- onCompletion { cause -> } — runs after flow completes (success or failure).
|
|
33
|
+
|
|
34
|
+
## Side Effects
|
|
35
|
+
- onEach { } — perform action on each element without transforming.
|
|
36
|
+
- onStart { } — runs before first element is emitted.
|
|
37
|
+
- onCompletion { } — runs after terminal event.
|
|
38
|
+
|
|
39
|
+
## Context
|
|
40
|
+
- flowOn(dispatcher) — change upstream dispatcher. Does NOT affect downstream.
|
|
41
|
+
- Never use withContext inside flow {} builder — use flowOn instead.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Flow Types
|
|
2
|
+
|
|
3
|
+
## Cold Flow
|
|
4
|
+
- flow {} builder: executes on each collector independently.
|
|
5
|
+
- Emissions are sequential within a single flow.
|
|
6
|
+
- Completes when the builder block finishes.
|
|
7
|
+
- Use for one-shot or on-demand data streams.
|
|
8
|
+
|
|
9
|
+
## StateFlow
|
|
10
|
+
- Hot flow — always has a current value (.value).
|
|
11
|
+
- Conflated: collectors always get the latest value, may skip intermediate.
|
|
12
|
+
- Requires initial value at creation.
|
|
13
|
+
- MutableStateFlow(initialValue) for read-write, expose as StateFlow for read-only.
|
|
14
|
+
- Equality-based deduplication: same value emitted twice is ignored.
|
|
15
|
+
- Use for UI state in ViewModels.
|
|
16
|
+
|
|
17
|
+
## SharedFlow
|
|
18
|
+
- Hot flow — event stream, no initial value required.
|
|
19
|
+
- Configurable replay (number of past values for new collectors) and extraBufferCapacity.
|
|
20
|
+
- MutableSharedFlow for read-write, expose as SharedFlow.
|
|
21
|
+
- replay = 0: new collectors miss past events (use for one-off events).
|
|
22
|
+
- Use for navigation events, snackbar triggers, analytics.
|
|
23
|
+
|
|
24
|
+
## Conversions
|
|
25
|
+
- stateIn(scope, started, initialValue): cold flow -> StateFlow.
|
|
26
|
+
- shareIn(scope, started, replay): cold flow -> SharedFlow.
|
|
27
|
+
- SharingStarted.WhileSubscribed(5_000): stops upstream 5s after last collector leaves — balance between freshness and resource savings.
|
|
28
|
+
- SharingStarted.Eagerly: starts immediately, never stops — use sparingly.
|
|
29
|
+
- SharingStarted.Lazily: starts on first collector, never stops.
|
|
30
|
+
|
|
31
|
+
## Special Builders
|
|
32
|
+
- callbackFlow {}: bridge callback APIs to Flow — use awaitClose {} for cleanup.
|
|
33
|
+
- channelFlow {}: concurrent emissions from multiple coroutines.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Structured Concurrency
|
|
2
|
+
|
|
3
|
+
- Every coroutine must have a parent scope — no orphan launches.
|
|
4
|
+
- CoroutineScope ties coroutine lifetime to a component (ViewModel, Activity, Service).
|
|
5
|
+
- Parent cancellation cascades to all children automatically.
|
|
6
|
+
- Child failure cancels siblings and parent (default Job behavior).
|
|
7
|
+
- SupervisorJob: child failure does NOT cancel siblings or parent.
|
|
8
|
+
- coroutineScope {} — creates child scope, waits for all children, propagates failure.
|
|
9
|
+
- supervisorScope {} — creates child scope, waits for all children, isolates failures.
|
|
10
|
+
- viewModelScope: auto-cancelled on ViewModel.onCleared().
|
|
11
|
+
- lifecycleScope: auto-cancelled on Lifecycle.DESTROYED.
|
|
12
|
+
- Custom scopes: create with CoroutineScope(SupervisorJob() + dispatcher), cancel explicitly.
|
|
13
|
+
|
|
14
|
+
## Cooperative Cancellation
|
|
15
|
+
- Check isActive in CPU-bound loops.
|
|
16
|
+
- Use ensureActive() for an automatic check + throw.
|
|
17
|
+
- yield() checks cancellation and yields to other coroutines.
|
|
18
|
+
- Never catch CancellationException — breaks cancellation propagation.
|
|
19
|
+
- If you must catch Exception, rethrow CancellationException.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Testing Coroutines
|
|
2
|
+
|
|
3
|
+
## Core Setup
|
|
4
|
+
- Use runTest {} (replaces deprecated runBlockingTest).
|
|
5
|
+
- runTest auto-advances virtual time for delay-based code.
|
|
6
|
+
- Inject TestDispatcher into classes under test.
|
|
7
|
+
|
|
8
|
+
## Test Dispatchers
|
|
9
|
+
- StandardTestDispatcher: does not auto-dispatch — control time with advanceTimeBy(), advanceUntilIdle().
|
|
10
|
+
- UnconfinedTestDispatcher: eager dispatch — coroutines run immediately.
|
|
11
|
+
- Use StandardTestDispatcher for precise timing tests.
|
|
12
|
+
- Use UnconfinedTestDispatcher when timing doesn't matter.
|
|
13
|
+
|
|
14
|
+
## Testing StateFlow
|
|
15
|
+
- Assert against .value directly after triggering actions.
|
|
16
|
+
- Use advanceUntilIdle() to flush pending coroutines.
|
|
17
|
+
- Set Dispatchers.Main via Dispatchers.setMain(testDispatcher) in @Before.
|
|
18
|
+
- Reset via Dispatchers.resetMain() in @After.
|
|
19
|
+
|
|
20
|
+
## Testing SharedFlow / Events
|
|
21
|
+
- Use Turbine library: flow.test { awaitItem(), expectNoEvents(), cancelAndIgnoreRemainingEvents() }.
|
|
22
|
+
- Turbine provides timeout-based assertions — no manual job juggling.
|
|
23
|
+
- awaitItem() suspends until next emission.
|
|
24
|
+
- expectNoEvents() asserts no emissions within timeout.
|
|
25
|
+
|
|
26
|
+
## Testing Cancellation
|
|
27
|
+
- Launch in a separate job, cancel it, verify cleanup ran.
|
|
28
|
+
- Use advanceTimeBy() to simulate timeout-based cancellation.
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-ai-skills",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "AI governance skills for Android,
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "AI governance skills for Android, KMP & Compose — works with Codex, Claude, Copilot, Cursor, Windsurf, Cline, JetBrains AI, Amazon Q & Aider.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/noloman/Android-AI-skills.git"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"bin": {
|
|
8
12
|
"android-ai-skills": "bin/android-ai-skills.js"
|
|
@@ -12,6 +16,7 @@
|
|
|
12
16
|
"compose-best-practices/",
|
|
13
17
|
"kmp-architecture-best-practices/",
|
|
14
18
|
"compose-multiplatform-best-practices/",
|
|
19
|
+
"kotlin-coroutines-best-practices/",
|
|
15
20
|
"README.md",
|
|
16
21
|
"LICENSE",
|
|
17
22
|
"CHANGELOG.md",
|