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 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
- [![CI](https://img.shields.io/github/actions/workflow/status/OWNER/REPO/release.yml?branch=main)](https://github.com/OWNER/REPO/actions)
1
+ [![CI](https://img.shields.io/github/actions/workflow/status/noloman/android-ai-skills/release.yml?branch=main)](https://github.com/noloman/android-ai-skills/actions)
2
2
  [![npm](https://img.shields.io/npm/v/android-ai-skills.svg)](https://www.npmjs.com/package/android-ai-skills)
3
3
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
4
 
5
- # 🚀 Android AI Architecture Skills
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
- This repository provides structured **AI governance layers** that enforce
11
- architecture, performance, and scalability best practices automatically.
10
+ Works with **Codex, Claude Code, GitHub Copilot, Cursor, Windsurf, Cline,
11
+ JetBrains AI, Amazon Q & Aider**.
12
12
 
13
13
  ---
14
14
 
15
- # 🎯 Why This Exists
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
- # 🧠 Skill Ecosystem
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
- # 📦 Included Skills
83
+ ## Included Skills
53
84
 
54
- ## 1️⃣ compose-best-practices
85
+ ### 1. compose-best-practices
55
86
 
56
87
  For Android-only Jetpack Compose apps.
57
88
 
58
- ### Enforces
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
- ## 2️⃣ kmp-architecture-best-practices
100
+ ### 2. kmp-architecture-best-practices
71
101
 
72
102
  For shared business logic in Kotlin Multiplatform.
73
103
 
74
- ### Enforces
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
- ## 3️⃣ compose-multiplatform-best-practices
114
+ ### 3. compose-multiplatform-best-practices
86
115
 
87
116
  For shared UI in commonMain using Compose Multiplatform.
88
117
 
89
- ### Enforces
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
- # 🔥 Enterprise Mode (Auto-Detection)
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
- # Performance Governance
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
- # 🏗 Architecture Governance
300
+ ## Architecture Governance
148
301
 
149
302
  ```mermaid
150
303
  flowchart TB
@@ -168,7 +321,7 @@ Principles:
168
321
 
169
322
  ---
170
323
 
171
- # 🧪 Stability & Compose Compiler Alignment
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
- # 📂 Repository Structure
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
- # 📖 How To Use
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.
@@ -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 Claude + Codex skills
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
- Install options:
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
- Target options:
32
- --target <both|codex|claude> Default: both
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 skill folders
40
- --print-paths Print resolved install paths and exit (or include in output)
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 both
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(["android-only","kmp-only","compose-mp-only","codex-only","claude-only","dry-run","force","help","print-paths"]);
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
- function resolveTargets(args) {
106
- const home = os.homedir();
107
- let target = (args.target || "both").toLowerCase();
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) return [path.resolve(process.cwd(), args.dest)];
247
+ if (args.dest) {
248
+ return [{ type: "codex", base: path.resolve(process.cwd(), args.dest) }];
249
+ }
112
250
 
113
- if (target === "both") return [path.join(home, ".codex", "skills"), path.join(home, ".claude", "skills")];
114
- if (target === "codex") return [path.join(home, ".codex", "skills")];
115
- if (target === "claude") return [path.join(home, ".claude", "skills")];
116
- throw new Error(`Unknown --target ${args.target}. Use both|codex|claude.`);
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 = resolveTargets(args);
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) console.log(` - ${path.join(t, s)}`);
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
- function writeAgentsMd(outDir, force) {
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
- ## Skill activation matrix
137
-
138
- If the project contains \`commonMain\` (KMP):
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
- - If shared UI composables exist in \`commonMain\`:
141
- - Use **compose-multiplatform-best-practices**
142
- - Else (shared business logic only):
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
- If the project is Android-only:
312
+ for (const toolName of tools) {
313
+ const cfg = INIT_TOOLS[toolName];
146
314
 
147
- - Use **compose-best-practices**
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
- ## Enterprise Mode auto-detection (optional)
320
+ if (fs.existsSync(filePath) && !force) {
321
+ console.log(`⏭️ Skipped ${path.relative(outDir, filePath)} (exists, use --force)`);
322
+ continue;
323
+ }
150
324
 
151
- Enterprise Mode turns on automatically when tooling is detected in the repo root:
152
- - detekt.yml / detekt.yaml
153
- - lint.xml
154
- - ktlint/spotless config
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
- If not detected, tool-specific enforcement is disabled (tool-agnostic guidance only).
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
- ensureDir(outDir, false);
176
- const p = writeAgentsMd(outDir, !!args.force);
177
- console.log(`✅ Wrote ${p}`);
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
- if (!uninstall) console.log("");
415
+ console.log("");
188
416
  }
189
417
 
190
418
  const skills = resolveSkills(args);
191
- const targets = resolveTargets(args);
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 baseDest of targets) {
198
- ensureDir(baseDest, dryRun);
199
- for (const skill of skills) {
200
- const src = path.join(projectRoot, skill);
201
- const dest = path.join(baseDest, skill);
202
- if (uninstall) {
203
- console.log(`🗑️ ${skill} → ${dest}`);
204
- removeDir(dest, { dryRun });
205
- } else {
206
- console.log(`✅ ${skill} ${dest}`);
207
- copyDir(src, dest, { dryRun, force });
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": "0.1.0",
4
- "description": "AI governance skills for Android, Kotlin Multiplatform (KMP), and Compose Multiplatform (Claude + Codex).",
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",