diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9f06139
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,32 @@
+name: ci
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: stable
+
+ - name: Validate skill
+ run: ./scripts/validate-skill.sh --quick --score-only
+
+ - name: Test
+ run: go test ./...
+
+ - name: Smoke test
+ run: ./scripts/smoke-test.sh
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f66d8fe..376940d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,98 +1,52 @@
-name: Release
+name: release
on:
push:
tags:
- - 'v[0-9]+.[0-9]+.[0-9]+'
- workflow_dispatch:
- inputs:
- version_bump:
- description: 'Version bump type'
- required: true
- default: 'patch'
- type: choice
- options:
- - patch
- - minor
- - major
+ - "v*"
permissions:
contents: write
jobs:
- validate:
- uses: ./.github/workflows/validate.yml
-
release:
- needs: validate
runs-on: ubuntu-latest
+
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Determine version
- id: version
- run: |
- if [[ "${{ github.event_name }}" == "push" ]]; then
- TAG="${GITHUB_REF#refs/tags/}"
- echo "new_tag=$TAG" >> "$GITHUB_OUTPUT"
- echo "Using pushed tag: $TAG"
- else
- LATEST=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
- if [ -z "$LATEST" ]; then
- MAJOR=1; MINOR=0; PATCH=0
- else
- VERSION="${LATEST#v}"
- MAJOR=$(echo "$VERSION" | cut -d. -f1)
- MINOR=$(echo "$VERSION" | cut -d. -f2)
- PATCH=$(echo "$VERSION" | cut -d. -f3)
-
- case "${{ inputs.version_bump }}" in
- major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
- minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
- patch) PATCH=$((PATCH + 1)) ;;
- esac
- fi
- NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
- echo "new_tag=$NEW_TAG" >> "$GITHUB_OUTPUT"
- echo "Computed next version: $NEW_TAG (bump: ${{ inputs.version_bump }})"
- fi
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: stable
- - name: Get release info
- id: release_info
- run: |
- MSG=$(git log -1 --pretty=format:'%s')
- echo "msg=$MSG" >> "$GITHUB_OUTPUT"
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v7
+ with:
+ distribution: goreleaser
+ version: "~> v2"
+ args: release --clean
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- DELIMITER="EOF_$(openssl rand -hex 8)"
- BODY=$(git log -1 --pretty=format:'%b')
- {
- echo "body<<$DELIMITER"
- printf '%s' "$BODY"
- echo ""
- echo "$DELIMITER"
- } >> "$GITHUB_OUTPUT"
+ - name: Package skills bundle
+ run: ./scripts/package-skills.sh "${GITHUB_REF_NAME}"
- - name: Create tag (manual dispatch only)
- if: github.event_name == 'workflow_dispatch'
+ - name: Generate skills bundle checksum
run: |
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
- git tag "${{ steps.version.outputs.new_tag }}"
- git push origin "${{ steps.version.outputs.new_tag }}"
-
- - name: Create GitHub Release
- uses: softprops/action-gh-release@v2
- with:
- tag_name: ${{ steps.version.outputs.new_tag }}
- name: ${{ steps.version.outputs.new_tag }}
- body: |
- **Commit:** `${{ github.sha }}`
- **Branch:** `${{ github.ref_name }}`
- **Author:** ${{ github.actor }}
-
- ${{ steps.release_info.outputs.body }}
- draft: false
- prerelease: false
+ cd dist
+ sha256sum "composekit-skills_${GITHUB_REF_NAME}.tar.gz" >> checksums.txt
+ cat checksums.txt
+
+ - name: Upload checksums (updated)
+ run: gh release upload "${GITHUB_REF_NAME}" "dist/checksums.txt" --clobber
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Upload skills bundle
+ run: gh release upload "${GITHUB_REF_NAME}" "dist/composekit-skills_${GITHUB_REF_NAME}.tar.gz" --clobber
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
deleted file mode 100644
index bbc752c..0000000
--- a/.github/workflows/validate.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: Validate Skill
-
-on:
- push:
- branches: [main]
- pull_request:
- workflow_call:
-
-jobs:
- validate:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v5
-
- - name: Run skill scanner
- run: ./scripts/validate.sh
diff --git a/.gitignore b/.gitignore
index e43b0f9..acd8178 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,24 @@
.DS_Store
+__MACOSX/
+
+# Go build output
+bin/
+dist/
+tmp/
+tmp-*/
+*.test
+*.out
+
+# Local smoke-test output
+tmp-smoke/
+tmp-skills/
+tmp-home/
+
+# Editor/local
+.idea/
+.vscode/
+.env
+.env.*
+
+# OS
+Thumbs.db
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
new file mode 100644
index 0000000..c1d10f4
--- /dev/null
+++ b/.goreleaser.yaml
@@ -0,0 +1,51 @@
+version: 2
+
+project_name: composekit
+
+before:
+ hooks:
+ - go mod tidy
+ - go test ./...
+ - ./scripts/validate-skill.sh --quick --score-only
+
+builds:
+ - id: composekit
+ main: .
+ binary: composekit
+ env:
+ - CGO_ENABLED=0
+ ldflags:
+ - -s -w
+ - -X main.version={{.Version}}
+ - -X main.commit={{.Commit}}
+ - -X main.date={{.Date}}
+ goos:
+ - darwin
+ - linux
+ - windows
+ goarch:
+ - amd64
+ - arm64
+
+archives:
+ - id: binaries
+ builds:
+ - composekit
+ name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
+ format_overrides:
+ - goos: windows
+ formats:
+ - zip
+ files:
+ - README.md
+ - LICENSE
+
+checksum:
+ name_template: "checksums.txt"
+
+changelog:
+ sort: asc
+
+release:
+ draft: false
+ prerelease: auto
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f340b4d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,21 @@
+APP := composekit
+
+.PHONY: build test smoke validate-skill clean snapshot
+
+build:
+ go build -o bin/$(APP) .
+
+test:
+ go test ./...
+
+smoke:
+ ./scripts/smoke-test.sh
+
+validate-skill:
+ ./scripts/validate-skill.sh
+
+clean:
+ rm -rf bin dist tmp-smoke tmp-skills
+
+snapshot:
+ goreleaser release --snapshot --clean
diff --git a/README.md b/README.md
index 63489d5..d0c1ff7 100644
--- a/README.md
+++ b/README.md
@@ -1,429 +1,94 @@
-
-
-
+# ComposeKit
-compose-skill
+ComposeKit is a CLI for installing and updating AI coding skills for Jetpack Compose, Compose Multiplatform, and Kotlin Multiplatform workflows.
-
- Make your AI coding tool actually understand Compose.
- A comprehensive agent skill for Jetpack Compose and Compose Multiplatform (KMP/CMP).
-
+## Install
-
-
-
-
-
-
-
-
----
-
-## What This Skill Does
-
-This is an **AI agent skill** β not a library, not documentation. Install it once, and your AI coding agent (Codex, Cursor, Claude Code) gains production-grade knowledge of the entire Compose app development lifecycle: architecture, UI, state, navigation, networking, persistence, performance, accessibility, cross-platform, build configuration, distribution, and code review.
-
-The skill covers **Android**, **iOS**, **Desktop**, and **Web** targets with the same architectural principles.
-
-## What's Covered
-
-
-
- | ποΈ Architecture & State |
- π¨ Compose UI |
- π Data & Networking |
-
-
-
- MVI with Event, State, Effect
- Unidirectional data flow
- ViewModel patterns
- State modeling (4 buckets)
- Clean code & anti-patterns
- |
-
- Three-phase model & side effects
- Coil 3 image loading
- Lists, grids, pagers & keying
- Shared element animations
- Material 3 theming & adaptive
- |
-
- Ktor HTTP client & auth flows
- DTO-to-domain mapping
- Room Database (KMP)
- DataStore (Preferences & Typed)
- Paging 3 with MVI
- |
-
- |
-
- | π§ Navigation & DI |
- β‘ Performance & Quality |
- π± Cross-Platform |
-
-
-
- Navigation 3 & Nav 2
- Tabs, scenes, deep links
- Koin (CMP) & Hilt (Android)
- ViewModel scoping
- |
-
- Recomposition minimization
- Compiler Metrics & profiles
- Turbine testing
- Macrobenchmark & UI tests
- Accessibility & WCAG
- |
-
- KMP commonMain sharing
- expect/actual patterns
- iOS interop (SKIE)
- CMP resources & localization
- Gradle/AGP 9+, CI/CD, signing
- |
-
-
-
-## Without vs With compose-skill
-
-| Concern | Without | With |
-|:--------|:--------|:-----|
-| **State management** | Scattered `mutableStateOf` in composables | Single `StateFlow` owned by ViewModel |
-| **Business logic** | Mixed into UI layer | Isolated in ViewModel's `onEvent()` handler |
-| **One-shot actions** | Boolean flags in state | `Channel` for navigation, snackbar |
-| **Recomposition** | Frequent, hard to diagnose | Minimized via state shape and read boundaries |
-| **Navigation** | Ad-hoc calls from composables | Semantic effects, route layer executes |
-| **Networking** | Inconsistent error handling | Ktor + `Result`/`ApiResult` wrapper, DTO mappers |
-| **Persistence** | Raw SharedPreferences | DataStore + Room with MVI integration |
-| **Accessibility** | Missing or incorrect semantics | `contentDescription`, touch targets, WCAG contrast |
-| **Cross-platform** | Android-only or inconsistent | `commonMain` with `expect/actual` for platform APIs |
-| **Build config** | Hardcoded versions | Version catalog, AGP 9+ patterns, conventions |
-| **Testing** | Manual UI testing | ViewModel eventβstateβeffect via Turbine |
-| **Code review** | Inconsistent patterns | Anti-pattern detection with documented fixes |
-
-## Installation
-
-Pick your agent and run **one command**. The clone target becomes the skill folder β agents detect `SKILL.md` at the root automatically.
-
-> **Only three things matter for the skill to work:** `SKILL.md`, `agents/`, and `references/`. Everything else in this repo (README, LICENSE, scripts, assets, .github) is for documentation, validation, and CI β the agent never reads them. If you prefer a minimal install, you only need those three.
-
-> **Why is `SKILL.md` at the root?** All three agents (Codex, Cursor, Claude Code) look for `SKILL.md` at the **top level** of the skill directory. This repo is structured so that cloning it directly into the skill path gives you a ready-to-use skill β no moving files or extra nesting required.
-
-> Skill installation paths may change as agents evolve. The locations below are accurate at the time of writing β for the latest instructions, refer to each agent's official docs or ask your agent *"How do I add a skill?"*
-> - [Codex Skills docs](https://developers.openai.com/codex/skills/) Β· [Cursor Skills docs](https://www.cursor.com/docs/context/skills) Β· [Claude Code Skills docs](https://code.claude.com/docs/en/slash-commands)
-
-### Quick Install (copy-paste)
-
-| Client | User-global | Per-repo |
-|:-------|:------------|:---------|
-| **Codex** | `~/.codex/skills/compose-skill` | `.codex/skills/compose-skill` |
-| **Cursor** | `~/.cursor/skills/compose-skill` | `.cursor/skills/compose-skill` |
-| **Claude Code** | `~/.claude/skills/compose-skill` | `.claude/skills/compose-skill` |
-| **Other agents** | Upload `SKILL.md`, `agents/`, and `references/` as project knowledge | β |
+### macOS/Linux
```bash
-# Replace with the install location from the table above
-git clone https://github.com/Meet-Miyani/compose-skill.git
-
-# Examples:
-git clone https://github.com/Meet-Miyani/compose-skill.git ~/.cursor/skills/compose-skill
-git clone https://github.com/Meet-Miyani/compose-skill.git .codex/skills/compose-skill
+curl -fsSL https://raw.githubusercontent.com/Meet-Miyani/composekit/main/install.sh | bash
```
-## Common Mistakes
-
-| Problem | Fix |
-|:--------|:----|
-| Folder named `compose-skill-main` | Rename to `compose-skill` (GitHub ZIP downloads add `-main`) |
-| `SKILL.md` not at root of skill folder | Don't nest inside another directory β clone directly into the skill path |
-| Skill not detected after install | Restart the agent / IDE |
+### Install and initialize
-## Verify Activation
-
-| Client | How to verify |
-|:-------|:--------------|
-| **Codex** | Run `/skills` β `compose-skill` appears in the list |
-| **Cursor** | **Settings β Rules** β skill appears under *Agent Decides* |
-| **Claude Code** | Run `/skills` or ask *"What skills are available?"* |
-
-## Usage
-
-Once installed, the skill activates **automatically** when your prompt matches its triggers (`@Composable`, `StateFlow`, `ViewModel`, `KMP`, `Ktor`, `recomposition`, `DataStore`, etc.). You can also invoke it **explicitly** β the syntax varies by client:
-
-| Client | Explicit invocation | Automatic |
-|:-------|:-------------------|:----------|
-| **Codex CLI** | `$compose-skill` in your prompt | Yes |
-| **Codex IDE extension** | `$compose-skill` in chat | Yes |
-| **Codex App** | `/compose-skill` in chat | Yes |
-| **Cursor** | `/compose-skill` in Agent chat | Yes |
-| **Claude Code** | `/compose-skill` in chat | Yes |
-
-### Invocation Examples
-
-**Codex CLI / IDE extension** β dollar-sign prefix:
-```text
-$compose-skill Refactor this screen to MVI with proper state modeling.
+```bash
+curl -fsSL https://raw.githubusercontent.com/Meet-Miyani/composekit/main/install.sh | bash -s -- --init
```
-**Codex App / Cursor / Claude Code** β slash prefix:
-```text
-/compose-skill How do I set up Paging 3 with MVI in a KMP project?
-```
+## Usage
-## Skill Structure
+Install the default Compose skill into detected AI coding agents:
-```text
-compose-skill/
-β
-β ## Required (the skill itself) βββββββββββββββββββββ
-βββ SKILL.md # Skill definition β agent reads this
-βββ agents/
-β βββ openai.yaml # Codex UI metadata
-βββ references/ # 37 deep-dive reference files
- β # (loaded on-demand by SKILL.md)
- βββ architecture.md
- βββ coroutines-flow.md
- βββ compose-essentials.md
- βββ material-design.md
- βββ image-loading.md
- βββ lists-grids.md
- βββ paging.md
- βββ paging-offline.md
- βββ paging-mvi-testing.md
- βββ navigation.md
- βββ navigation-3.md
- βββ navigation-2.md
- βββ navigation-3-di.md
- βββ navigation-2-di.md
- βββ navigation-migration.md
- βββ performance.md
- βββ animations.md
- βββ ui-ux.md
- βββ testing.md
- βββ room-database.md
- βββ datastore.md
- βββ networking-ktor.md
- βββ networking-ktor-auth.md
- βββ networking-ktor-testing.md
- βββ networking-ktor-architecture.md
- βββ dependency-injection.md
- βββ koin.md
- βββ hilt.md
- βββ cross-platform.md
- βββ resources.md
- βββ ios-swift-interop.md
- βββ accessibility.md
- βββ clean-code.md
- βββ anti-patterns.md
- βββ gradle-build.md
- βββ ci-cd-distribution.md
-β
-β ## Optional (repo extras) ββββββββββββββββββββββββββ
-βββ README.md # This file (not read by agents)
-βββ LICENSE # MIT License
-βββ assets/
-β βββ compose-multiplatform-icon.svg # Logo for README
-βββ scripts/
- βββ validate.sh # Skill scanner / validation tool
+```bash
+composekit init
```
-## Reference Guide
-
-The `references/` directory contains 37 deep-dive files that the skill loads on-demand. Here's what each one covers in detail:
-
-
-Kotlin Foundations
-
-| Reference | What's Inside |
-|:----------|:-------------|
-| **coroutines-flow.md** | StateFlow vs SharedFlow vs Channel decision table, Flow operators (`flatMapLatest`, `combine`, `debounce`, `catch`), Dispatchers (IO/Default/Main), structured concurrency (`viewModelScope`, `supervisorScope`), exception handling, `CancellationException`, `stateIn`/`shareIn`, backpressure (`buffer`/`conflate`/`collectLatest`), `callbackFlow`, Mutex/Semaphore, testing with Turbine |
-
-
-
-
-Architecture
+Update installed skills to the latest release:
-| Reference | What's Inside |
-|:----------|:-------------|
-| **architecture.md** | ViewModel/event-handling pipeline, state modeling, Channel vs SharedFlow for effects, domain layer rules, inter-feature communication (event bus, feature API contracts), module dependency rules, GOOD/BAD code examples |
-| **clean-code.md** | Avoiding overengineering, file organization, naming conventions, disciplined vs bloated MVI comparison |
-| **anti-patterns.md** | Cross-cutting anti-pattern quick-reference table with "why it hurts" and "better replacement" for each, plus routing index to domain-specific anti-patterns in other reference files |
-
-
-
-
-Compose APIs
-
-| Reference | What's Inside |
-|:----------|:-------------|
-| **material-design.md** | M3 theme setup (dynamic color, dark/light, color roles), typography/shapes, component decisions (Scaffold, TopAppBar, NavigationBar/Rail/Suite, BottomSheet, Snackbar, Dialog), adaptive layouts (window size classes, canonical layouts), M2βM3 migration |
-| **image-loading.md** | Coil 3 setup for Compose/CMP, `AsyncImage`/`rememberAsyncImagePainter`/`SubcomposeAsyncImage` decision guide, placeholder/error/fallback/crossfade, memory/disk/network cache policy, transformations vs `Modifier.clip`, SVG (`coil-svg`), `Res.getUri` resource loading |
-| **compose-essentials.md** | Three phases model, state primitives, side effects (`LaunchedEffect`, `DisposableEffect`, `rememberUpdatedState`), modifier ordering, `graphicsLayer`, slot pattern, `CompositionLocal`, `collectAsStateWithLifecycle` |
-| **lists-grids.md** | LazyColumn/LazyRow, keys, `contentType`, grids, pager, scroll state, nested scrolling, list anti-patterns |
-| **paging.md** | PagingSource, Pager + ViewModel setup (PagingData as separate Flow, never in UiState), `cachedIn`, filter/search with `flatMapLatest`, `LazyPagingItems` (all lazy layouts), LoadState handling, PagingData transformations, `PagingSource.invalidate()` |
-| **paging-offline.md** | RemoteMediator offline-first with Room, `initialize()` (`LAUNCH_INITIAL_REFRESH` vs `SKIP_INITIAL_REFRESH`), remote keys, Pager wiring |
-| **paging-mvi-testing.md** | MVI dual-flow pattern (PagingData separate from UiState), route collection, PagingSource unit tests, `asSnapshot`, `TestPager`, anti-patterns table |
-| **navigation.md** | Shared navigation concepts: Nav 2 vs Nav 3 decision guide, MVI navigation rules (both versions), anti-patterns table, routing to version-specific files |
-| **navigation-3.md** | Nav 3 full reference: route definition, back stack persistence, `NavDisplay` full API, top-level tabs (`NavigationSuiteScaffold`), ViewModel scoping with entry decorators, Scenes (dialog, bottom sheet, list-detail, Material Adaptive), animations, back stack manipulation, deep links, CMP polymorphic serialization |
-| **navigation-2.md** | Nav 2 full reference: `NavHost`/`NavController`, type-safe routes (2.8+), string routes, top-level tabs (`NavigationBar` + `currentBackStackEntryAsState` + `saveState`/`restoreState`), deep links (`NavDeepLink`), navigate with results (`SavedStateHandle`), nested graphs, animations (`enterTransition`/`exitTransition`), conditional navigation (auth guards), predictive back |
-| **navigation-3-di.md** | Nav 3 + DI wiring: Hilt `hiltViewModel` in entry blocks + `@AssistedInject` + multibinding entry providers, Koin `navigation` DSL + `koinEntryProvider()`, modularization api/impl split, entry-scoped VMs via decorators |
-| **navigation-2-di.md** | Nav 2 + DI wiring: Hilt `hiltViewModel` in composable destinations + graph-scoped VMs via `getBackStackEntry` + `SavedStateHandle`, Koin `koinViewModel` + `koinNavViewModel` + `sharedKoinViewModel` for graph-scoped sharing |
-| **navigation-migration.md** | Nav 2 to Nav 3 migration: conceptual shift table, step-by-step migration (routes β NavKey, controller β backStack, NavHost β NavDisplay, graph VMs β entry decorators, deep links, tabs), incremental strategy, coexistence |
-
-
-
-
-Performance & Quality
-
-| Reference | What's Inside |
-|:----------|:-------------|
-| **performance.md** | Three phases, primitive state specializations, `TextFieldState`, Strong Skipping Mode, stability config, Compose Compiler Metrics, baseline profiles, API decision table, 20 recomposition rules, diagnostic checklist |
-| **animations.md** | Complete animation API reference: decision tree, `AnimationSpec` (spring/tween/keyframes), `animate*AsState`, `Animatable` (sequential, concurrent, gesture-driven), `updateTransition`, `AnimatedVisibility`, `AnimatedContent`, shared element transitions with navigation and Coil, swipe-to-dismiss, Canvas/custom drawing, `graphicsLayer`, performance optimization |
-| **ui-ux.md** | Loading states, skeleton/shimmer, preserving content during refresh, inline validation, perceived performance |
-| **accessibility.md** | `contentDescription` rules, `Modifier.semantics` (role, stateDescription, heading), `mergeDescendants`, `clearAndSetSemantics`, touch targets (48dp), WCAG color contrast, custom interactive elements, custom accessibility actions |
-| **testing.md** | Turbine for StateFlow testing, ViewModel eventβstateβeffect testing, validation/UI tests, Macrobenchmark, lean test matrix by app scale |
-
-
-
-
-Data & Persistence
-
-| Reference | What's Inside |
-|:----------|:-------------|
-| **datastore.md** | KMP + Android setup, Preferences DataStore keys/read/write, Typed DataStore with JSON serialization, singleton enforcement, corruption handling, SharedPreferences migration, MVI integration, DI wiring, testing, anti-patterns |
-| **room-database.md** | Entity design, performance-oriented DAOs, indexes, relationships (`@Embedded`/`@Relation`/`@Junction`), TypeConverters, transactions, migrations, MVI integration, anti-patterns |
-
-
-
-
-Networking, DI & Cross-Platform
-
-| Reference | What's Inside |
-|:----------|:-------------|
-| **networking-ktor.md** | HttpClient configuration, platform engines, plugins (ContentNegotiation, Retry, Timeout, Logging, ContentEncoding), custom plugins (`createClientPlugin`), DTOs, mappers, API service (CRUD, multipart), repository pattern, proxy/SSL |
-| **networking-ktor-auth.md** | Bearer token auth with refresh, WebSockets (frames, serialization converter, session), SSE (Server-Sent Events) |
-| **networking-ktor-testing.md** | MockEngine setup (success, error, request assertions, multiple responses), engine injection, Koin/Hilt DI integration, testing anti-patterns |
-| **networking-ktor-architecture.md** | `Result` vs `ApiResult` decision, `safeRequest` wrapper, exception classification, plugin composition strategy, client factory design, response observation plugin, debug vs production, architecture anti-patterns |
-| **dependency-injection.md** | DI decision guide (Hilt vs Koin), shared concepts |
-| **koin.md** | Koin setup for CMP and Android, module organization, `koinViewModel`, `koinInject`, Koin + Nav 3 (`navigation`, `koinEntryProvider`), scoped navigation, MVI ViewModel integration, testing |
-| **hilt.md** | Android-only Hilt setup, `@HiltViewModel`, `hiltViewModel()`, modules (`@Provides`/`@Binds`), scopes, Navigation Compose integration, MVI pattern with Hilt, testing |
-| **cross-platform.md** | `commonMain` vs platform placement, interfaces vs `expect/actual`, platform bridge patterns (interface+DI, expect/actual, typealias), lifecycle, state restoration, resources, accessibility |
-| **ios-swift-interop.md** | KotlinβSwift naming, nullability/collection bridging, SKIE setup, suspendβasync, FlowβAsyncSequence, sealed class mapping, SwiftUI/UIKit interop (`ComposeUIViewController`, `UIKitView`), iOS API design rules |
-| **resources.md** | Android `R` vs CMP `Res` comparison, `composeResources/` directory structure, Gradle setup, drawable/string/plural/font/raw-file APIs with code examples, qualifiers (language, theme, density), localization, generated resource maps, Android assets interop (`Res.getUri`), MVI integration |
-
-
-
-
-Build, Distribution & CI/CD
-
-| Reference | What's Inside |
-|:----------|:-------------|
-| **gradle-build.md** | AGP 9+ project structure, version catalog (`[versions]`/`[libraries]`/`[plugins]`/`[bundles]`), bundle patterns, composite builds (`includeBuild` + `dependencySubstitution`), private Maven repos, `settings.gradle.kts`, `gradle.properties`, module-level build scripts, `compileSdk { version = release(N) }`, KSP/Room/Koin wiring, convention plugins guidance |
-| **ci-cd-distribution.md** | GitHub Actions workflows (Android APK, Desktop multi-OS DMG/MSI/DEB), desktop app module setup (`compose.desktop`), iOS Xcode framework integration, signing/notarization (Android/macOS/iOS), adding JVM desktop target to existing CMP project, Gradle task reference table |
-
-
-
-## Example Prompts
-
-
-Architecture & State
-
-```text
-Refactor this Compose screen to MVI.
-How should I structure a KMP feature module with Compose UI and ViewModel?
-Audit this feature against the compose-skill and list anti-patterns first, then apply minimal fixes.
+```bash
+composekit update
```
-
-
-UI & Performance
+List available skills:
-```text
-I have too much recomposition in this form screen. What should I change?
-Optimize recomposition in this screen and explain each state-shape change.
-Add shared element transitions between my list and detail screens.
+```bash
+composekit skills list
```
-
-
-Data & Networking
+Find skills:
-```text
-Set up Ktor with bearer token auth and refresh for my KMP project.
-Should this be SharedFlow or Channel for one-off effects?
-How do I use DataStore for user preferences in a KMP app?
+```bash
+composekit skills find navigation
```
-
-
-Cross-Platform & Distribution
+Detect supported agent skill directories:
-```text
-How do I expose this Kotlin StateFlow to Swift using SKIE?
-Set up GitHub Actions to build DMG, MSI, and DEB for my desktop app.
-Add iOS target to my existing Compose Multiplatform project.
+```bash
+composekit targets detect
```
-
-
-Accessibility & Quality
+## Commands
+
+| Command | Description |
+|:--------|:------------|
+| `composekit init` | Install the Compose skill to detected targets |
+| `composekit update` | Update installed skills |
+| `composekit doctor` | Check installation status |
+| `composekit remove` | Remove installed skills |
+| `composekit version` | Print CLI version |
+| `composekit skills list` | List available skills |
+| `composekit skills find ` | Search skills by name, description, or keywords |
+| `composekit skills add ` | Install a specific skill |
+| `composekit skills remove ` | Remove a specific skill |
+| `composekit skills installed` | List installed skills |
+| `composekit targets detect` | Detect supported agent skill directories |
+| `composekit targets list` | List saved targets |
+| `composekit targets add ` | Add a custom target directory |
+| `composekit targets remove ` | Remove a custom target directory |
+
+## Supported agent skill directories
+
+ComposeKit detects and installs skills into:
+
+| Agent | Path |
+|---|---|
+| Antigravity | `~/.gemini/antigravity/skills` |
+| Claude | `~/.claude/skills` |
+| Codex | `~/.codex/skills` |
+| Cursor | `~/.cursor/skills` |
+| Firebender | `~/.firebender/skills` |
+| Gemini | `~/.gemini/skills` |
+| OpenCode | `~/.config/opencode/skills` |
+
+## Building from source
-```text
-Review this screen for accessibility issues.
-How do I make my custom interactive component accessible?
-Set up ViewModel tests with Turbine for this feature.
+```bash
+git clone https://github.com/Meet-Miyani/composekit.git
+cd composekit
+go build -o bin/composekit .
```
-
-
-## Official Documentation
-
-| Resource | Link |
-|:---------|:-----|
-| Jetpack Compose | [developer.android.com/compose](https://developer.android.com/develop/ui/compose) |
-| Compose Multiplatform | [jetbrains.com/compose-multiplatform](https://www.jetbrains.com/compose-multiplatform/) |
-| Kotlin Coroutines | [kotlinlang.org/coroutines](https://kotlinlang.org/docs/coroutines-overview.html) |
-| StateFlow & SharedFlow | [kotlinlang.org/flow](https://kotlinlang.org/docs/flow.html#stateflow-and-sharedflow) |
-| ViewModel | [developer.android.com/viewmodel](https://developer.android.com/topic/libraries/architecture/viewmodel) |
-| Navigation 3 | [developer.android.com/navigation](https://developer.android.com/develop/ui/compose/navigation) |
-| Coil | [coil-kt.github.io/coil](https://coil-kt.github.io/coil/) |
-| Paging 3 | [developer.android.com/paging](https://developer.android.com/topic/libraries/architecture/paging/v3-overview) |
-| Room | [developer.android.com/room](https://developer.android.com/training/data-storage/room) |
-| DataStore | [developer.android.com/datastore](https://developer.android.com/topic/libraries/architecture/datastore) |
-| Ktor Client | [ktor.io/docs/client](https://ktor.io/docs/client.html) |
-| Koin | [insert-koin.io](https://insert-koin.io/docs/reference/koin-compose/compose/) |
-| Hilt | [developer.android.com/hilt](https://developer.android.com/training/dependency-injection/hilt-android) |
-| Agent Skills Standard | [agentskills.io](https://agentskills.io/) |
-
-## Contributing
-
-Contributions are welcome! Whether it's fixing a typo, improving a reference doc, or adding coverage for a new Compose API β all help is appreciated.
-
-1. **Fork** the repository
-2. **Create a branch** for your change (`git checkout -b improve-navigation-docs`)
-3. **Make your changes** β keep reference files focused and under 500 lines where possible
-4. **Run the scanner** to verify everything passes:
- ```bash
- ./scripts/validate.sh
- ```
-5. **Open a pull request** with a clear description of what changed and why
-
-### Guidelines
-
-- Follow the [agentskills.io specification](https://agentskills.io/specification) for any structural changes
-- Keep `SKILL.md` body under 500 lines β move detailed content to `references/`
-- Every reference file in `references/` should be linked from `SKILL.md`
-- Use code examples that compile and follow the skill's MVI conventions
-- Don't add dependencies β the skill is pure markdown and bash
## License
-This project is licensed under the [MIT License](LICENSE).
-
----
-
-
- Built for Jetpack Compose and Compose Multiplatform.
- Works with Codex, Cursor, Claude Code, and any Agent Skills-compatible tool.
-
+MIT License β see [LICENSE](LICENSE).
diff --git a/agents/openai.yaml b/agents/openai.yaml
deleted file mode 100644
index e5500d4..0000000
--- a/agents/openai.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-interface:
- display_name: "compose-skill"
- short_description: "AI agent skill for Jetpack Compose and Compose Multiplatform β architecture, state, navigation, DI, performance, cross-platform, and code review"
- default_prompt: "Use $compose-skill to build, refactor, or review Compose/CMP features, adapting to the project's architecture (MVI recommended for new work), and verifying dependencies before recommending them."
diff --git a/catalog/skills.json b/catalog/skills.json
new file mode 100644
index 0000000..7ae511d
--- /dev/null
+++ b/catalog/skills.json
@@ -0,0 +1,35 @@
+{
+ "schemaVersion": 1,
+ "skills": [
+ {
+ "name": "compose",
+ "path": "skills/compose",
+ "displayName": "Compose Multiplatform",
+ "description": "Production-grade Jetpack Compose and Compose Multiplatform guidance for Kotlin, KMP, CMP, architecture, state management, navigation, networking, persistence, performance, accessibility, testing, previews, and cross-platform UI delivery.",
+ "keywords": [
+ "compose",
+ "jetpack-compose",
+ "compose-multiplatform",
+ "cmp",
+ "kmp",
+ "kotlin",
+ "kotlin-multiplatform",
+ "android",
+ "ios",
+ "desktop",
+ "navigation",
+ "ui",
+ "state",
+ "mvi",
+ "mvvm",
+ "ktor",
+ "room",
+ "datastore",
+ "sql",
+ "testing",
+ "performance",
+ "accessibility"
+ ]
+ }
+ ]
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..73df5b4
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/Meet-Miyani/composekit
+
+go 1.23
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..ba0ac6c
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,114 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+REPO="${COMPOSEKIT_REPO:-Meet-Miyani/composekit}"
+INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
+BINARY="composekit"
+
+os="$(uname -s | tr '[:upper:]' '[:lower:]')"
+arch="$(uname -m)"
+
+case "$os" in
+ darwin) os="darwin" ;;
+ linux) os="linux" ;;
+ *) echo "Unsupported OS: $os"; exit 1 ;;
+esac
+
+case "$arch" in
+ x86_64|amd64) arch="amd64" ;;
+ arm64|aarch64) arch="arm64" ;;
+ *) echo "Unsupported architecture: $arch"; exit 1 ;;
+esac
+
+tmp="$(mktemp -d)"
+trap 'rm -rf "$tmp"' EXIT
+
+api="https://api.github.com/repos/${REPO}/releases/latest"
+release_json="$tmp/release.json"
+
+curl -fsSL "$api" -o "$release_json"
+
+asset_url="$(grep -o '"browser_download_url": "[^"]*"' "$release_json" \
+ | cut -d'"' -f4 \
+ | grep "${os}_${arch}" \
+ | head -n 1)"
+
+checksums_url="$(grep -o '"browser_download_url": "[^"]*"' "$release_json" \
+ | cut -d'"' -f4 \
+ | grep 'checksums.txt' \
+ | head -n 1)"
+
+if [ -z "${asset_url:-}" ]; then
+ echo "Could not find release asset for ${os}_${arch}"
+ exit 1
+fi
+
+if [ -z "${checksums_url:-}" ]; then
+ echo "Could not find checksums.txt"
+ exit 1
+fi
+
+archive="$tmp/archive"
+checksums="$tmp/checksums.txt"
+
+curl -fsSL "$asset_url" -o "$archive"
+curl -fsSL "$checksums_url" -o "$checksums"
+
+archive_name="$(basename "$asset_url")"
+if command -v sha256sum >/dev/null 2>&1; then
+ sha_cmd="sha256sum"
+else
+ sha_cmd="shasum -a 256"
+fi
+actual_sha="$($sha_cmd "$archive" | awk '{print $1}')"
+expected_sha="$(grep "$archive_name" "$checksums" | awk '{print $1}')"
+
+if [ "$actual_sha" != "$expected_sha" ]; then
+ echo "Checksum verification failed"
+ echo "expected: $expected_sha"
+ echo "actual: $actual_sha"
+ exit 1
+fi
+
+mkdir -p "$tmp/extract"
+case "$archive_name" in
+ *.tar.gz) tar -xzf "$archive" -C "$tmp/extract" ;;
+ *.zip) unzip -q "$archive" -d "$tmp/extract" ;;
+ *) echo "Unsupported archive format: $archive_name"; exit 1 ;;
+esac
+
+mkdir -p "$INSTALL_DIR"
+found_binary="$(find "$tmp/extract" -type f -name "$BINARY" -perm -111 | head -n 1)"
+
+if [ -z "${found_binary:-}" ]; then
+ found_binary="$(find "$tmp/extract" -type f -name "$BINARY" | head -n 1)"
+fi
+
+if [ -z "${found_binary:-}" ]; then
+ echo "Could not find $BINARY in archive"
+ exit 1
+fi
+
+cp "$found_binary" "$INSTALL_DIR/$BINARY"
+chmod +x "$INSTALL_DIR/$BINARY"
+
+echo "Installed $BINARY to $INSTALL_DIR/$BINARY"
+
+# Run init if --init flag is passed
+for arg in "$@"; do
+ if [ "$arg" = "--init" ]; then
+ echo ""
+ echo "Running composekit init..."
+ "$INSTALL_DIR/$BINARY" init
+ break
+ fi
+done
+
+case ":$PATH:" in
+ *":$INSTALL_DIR:"*) ;;
+ *)
+ echo ""
+ echo "Add this to your shell profile if needed:"
+ echo " export PATH=\"$INSTALL_DIR:\$PATH\""
+ ;;
+esac
diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go
new file mode 100644
index 0000000..7c411d1
--- /dev/null
+++ b/internal/catalog/catalog.go
@@ -0,0 +1,131 @@
+package catalog
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/fs"
+ "strings"
+
+ "github.com/Meet-Miyani/composekit/internal/targets"
+)
+
+type SkillEntry struct {
+ Name string `json:"name"`
+ Path string `json:"path"`
+ DisplayName string `json:"displayName"`
+ Description string `json:"description"`
+ Keywords []string `json:"keywords"`
+}
+
+type Catalog struct {
+ SchemaVersion int `json:"schemaVersion"`
+ Skills []SkillEntry `json:"skills"`
+}
+
+func Load(efs fs.FS, path string) (*Catalog, error) {
+ data, err := fs.ReadFile(efs, path)
+ if err != nil {
+ return nil, err
+ }
+ var c Catalog
+ if err := json.Unmarshal(data, &c); err != nil {
+ return nil, err
+ }
+ return &c, nil
+}
+
+func (c *Catalog) FindByQuery(query string) []SkillEntry {
+ q := strings.ToLower(query)
+ var results []SkillEntry
+ for _, s := range c.Skills {
+ if strings.Contains(strings.ToLower(s.Name), q) {
+ results = append(results, s)
+ continue
+ }
+ if strings.Contains(strings.ToLower(s.DisplayName), q) {
+ results = append(results, s)
+ continue
+ }
+ if strings.Contains(strings.ToLower(s.Description), q) {
+ results = append(results, s)
+ continue
+ }
+ for _, kw := range s.Keywords {
+ if strings.Contains(strings.ToLower(kw), q) {
+ results = append(results, s)
+ break
+ }
+ }
+ }
+ return results
+}
+
+func (c *Catalog) FindByName(name string) *SkillEntry {
+ for _, s := range c.Skills {
+ if s.Name == name {
+ return &s
+ }
+ }
+ return nil
+}
+
+type InstallStatus struct {
+ Name string `json:"name"`
+ Installed bool `json:"installed"`
+ Path string `json:"path"`
+}
+
+func (c *Catalog) PrintListShort() {
+ fmt.Println("Available skills:")
+ fmt.Println()
+ for _, s := range c.Skills {
+ fmt.Printf(" %s\n", s.Name)
+ }
+}
+
+func (c *Catalog) PrintSkillsLong(statuses map[string][]InstallStatus) {
+ for _, s := range c.Skills {
+ fmt.Printf("%s\n", s.Name)
+ fmt.Printf(" Display name: %s\n", s.DisplayName)
+ fmt.Printf(" Description: %s\n", s.Description)
+ fmt.Printf(" Keywords: %s\n", strings.Join(s.Keywords, ", "))
+ fmt.Println(" Installed:")
+ if sts, ok := statuses[s.Name]; ok {
+ for _, st := range sts {
+ inst := "no"
+ if st.Installed {
+ inst = "yes"
+ }
+ agentName := targets.AgentNameForPath(st.Name)
+ fmt.Printf(" %-12s %-4s %s\n", agentName, inst, st.Path)
+ }
+ }
+ fmt.Println()
+ }
+}
+
+func (c *Catalog) PrintFindResults(query string, results []SkillEntry) {
+ if len(results) == 0 {
+ fmt.Printf("No skills found matching '%s'\n", query)
+ return
+ }
+ for _, s := range results {
+ fmt.Printf("%s\n", s.Name)
+ fmt.Printf(" %s\n", s.DisplayName)
+ q := strings.ToLower(query)
+ if strings.Contains(strings.ToLower(s.Name), q) {
+ fmt.Printf(" Matched: name\n")
+ } else if strings.Contains(strings.ToLower(s.DisplayName), q) {
+ fmt.Printf(" Matched: display name\n")
+ } else if strings.Contains(strings.ToLower(s.Description), q) {
+ fmt.Printf(" Matched: description\n")
+ } else {
+ for _, kw := range s.Keywords {
+ if strings.Contains(strings.ToLower(kw), q) {
+ fmt.Printf(" Matched: keyword %s\n", kw)
+ break
+ }
+ }
+ }
+ }
+}
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..fd3bfd0
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,65 @@
+package config
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+
+ "github.com/Meet-Miyani/composekit/internal/targets"
+)
+
+type Config struct {
+ Targets []string `json:"targets"`
+}
+
+func ConfigDir() (string, error) {
+ if v := os.Getenv("COMPOSEKIT_CONFIG_HOME"); v != "" {
+ return v, nil
+ }
+ userDir, err := os.UserConfigDir()
+ if err != nil {
+ return "", err
+ }
+ dir := filepath.Join(userDir, "composekit")
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return "", err
+ }
+ return dir, nil
+}
+
+func ConfigPath() (string, error) {
+ dir, err := ConfigDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(dir, "config.json"), nil
+}
+
+func Load() *Config {
+ cfg := &Config{}
+ p, err := ConfigPath()
+ if err != nil {
+ return cfg
+ }
+ data, err := os.ReadFile(p)
+ if err != nil {
+ return cfg
+ }
+ json.Unmarshal(data, cfg)
+ for i, t := range cfg.Targets {
+ cfg.Targets[i] = targets.ExpandHome(t)
+ }
+ return cfg
+}
+
+func Save(cfg *Config) error {
+ p, err := ConfigPath()
+ if err != nil {
+ return err
+ }
+ data, err := json.MarshalIndent(cfg, "", " ")
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(p, data, 0644)
+}
diff --git a/internal/github/releases.go b/internal/github/releases.go
new file mode 100644
index 0000000..6ea6da0
--- /dev/null
+++ b/internal/github/releases.go
@@ -0,0 +1,199 @@
+package github
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "crypto/sha256"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+type ReleaseAsset struct {
+ Name string `json:"name"`
+ URL string `json:"browser_download_url"`
+}
+
+type GitHubRelease struct {
+ TagName string `json:"tag_name"`
+ Assets []ReleaseAsset `json:"assets"`
+}
+
+func Repo() string {
+ if v := os.Getenv("COMPOSEKIT_REPO"); v != "" {
+ return v
+ }
+ return "Meet-Miyani/composekit"
+}
+
+func FetchLatestRelease() (*GitHubRelease, error) {
+ apiURL := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", Repo())
+ resp, err := http.Get(apiURL)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("GitHub API returned %d", resp.StatusCode)
+ }
+ var release GitHubRelease
+ if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
+ return nil, err
+ }
+ return &release, nil
+}
+
+func FindAsset(release *GitHubRelease, pattern string) *ReleaseAsset {
+ for _, a := range release.Assets {
+ if strings.Contains(a.Name, pattern) {
+ return &a
+ }
+ }
+ return nil
+}
+
+func DownloadFile(url, destination string) error {
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != 200 {
+ return fmt.Errorf("download returned %d for %s", resp.StatusCode, url)
+ }
+ if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
+ return err
+ }
+ out, err := os.Create(destination)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ _, err = io.Copy(out, resp.Body)
+ return err
+}
+
+func VerifyChecksum(filePath, expectedHex string) error {
+ data, err := os.ReadFile(filePath)
+ if err != nil {
+ return err
+ }
+ sum := fmt.Sprintf("%x", sha256.Sum256(data))
+ if sum != expectedHex {
+ return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedHex, sum)
+ }
+ return nil
+}
+
+func ParseChecksums(checksumsPath string) (map[string]string, error) {
+ data, err := os.ReadFile(checksumsPath)
+ if err != nil {
+ return nil, err
+ }
+ result := make(map[string]string)
+ for _, line := range strings.Split(string(data), "\n") {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ parts := strings.Fields(line)
+ if len(parts) >= 2 {
+ result[parts[1]] = parts[0]
+ }
+ }
+ return result, nil
+}
+
+func ExtractTarGz(archivePath, destDir string) error {
+ if err := os.MkdirAll(destDir, 0755); err != nil {
+ return err
+ }
+ f, err := os.Open(archivePath)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ gzr, err := gzip.NewReader(f)
+ if err != nil {
+ return err
+ }
+ defer gzr.Close()
+
+ tr := tar.NewReader(gzr)
+ for {
+ header, err := tr.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return err
+ }
+ target := filepath.Join(destDir, header.Name)
+ switch header.Typeflag {
+ case tar.TypeDir:
+ if err := os.MkdirAll(target, 0755); err != nil {
+ return err
+ }
+ case tar.TypeReg:
+ if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
+ return err
+ }
+ out, err := os.Create(target)
+ if err != nil {
+ return err
+ }
+ if _, err := io.Copy(out, tr); err != nil {
+ out.Close()
+ return err
+ }
+ out.Close()
+ }
+ }
+ return nil
+}
+
+func DownloadSkillsBundle(release *GitHubRelease, tmpDir string) (string, error) {
+ bundlePattern := fmt.Sprintf("composekit-skills_%s.tar.gz", release.TagName)
+ asset := FindAsset(release, bundlePattern)
+ if asset == nil {
+ bundlePattern = "composekit-skills_"
+ asset = FindAsset(release, bundlePattern)
+ }
+ if asset == nil {
+ return "", fmt.Errorf("no skills bundle found in release %s", release.TagName)
+ }
+
+ archivePath := filepath.Join(tmpDir, asset.Name)
+ if err := DownloadFile(asset.URL, archivePath); err != nil {
+ return "", fmt.Errorf("download skills bundle: %w", err)
+ }
+
+ checksumsAsset := FindAsset(release, "checksums.txt")
+ if checksumsAsset != nil {
+ checksumsPath := filepath.Join(tmpDir, "checksums.txt")
+ if err := DownloadFile(checksumsAsset.URL, checksumsPath); err != nil {
+ return "", fmt.Errorf("download checksums: %w", err)
+ }
+ checksums, err := ParseChecksums(checksumsPath)
+ if err != nil {
+ return "", fmt.Errorf("parse checksums: %w", err)
+ }
+ if expectedHex, ok := checksums[asset.Name]; ok {
+ if err := VerifyChecksum(archivePath, expectedHex); err != nil {
+ return "", fmt.Errorf("checksum verification: %w", err)
+ }
+ }
+ }
+
+ extractDir := filepath.Join(tmpDir, "skills-bundle")
+ if err := ExtractTarGz(archivePath, extractDir); err != nil {
+ return "", fmt.Errorf("extract skills bundle: %w", err)
+ }
+
+ return extractDir, nil
+}
diff --git a/internal/install/install.go b/internal/install/install.go
new file mode 100644
index 0000000..c2c4f3c
--- /dev/null
+++ b/internal/install/install.go
@@ -0,0 +1,152 @@
+package install
+
+import (
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+
+ "github.com/Meet-Miyani/composekit/internal/manifest"
+)
+
+func CopyPayload(efs fs.FS, payloadRoot, dest string) error {
+ if err := os.MkdirAll(dest, 0755); err != nil {
+ return err
+ }
+ return fs.WalkDir(efs, payloadRoot, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ rel, err := filepath.Rel(payloadRoot, path)
+ if err != nil {
+ return err
+ }
+ if rel == "." {
+ return nil
+ }
+ out := filepath.Join(dest, rel)
+ if d.IsDir() {
+ return os.MkdirAll(out, 0755)
+ }
+ data, err := fs.ReadFile(efs, path)
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(out, data, 0644)
+ })
+}
+
+func CopyDir(srcDir, dest string) error {
+ if err := os.MkdirAll(dest, 0755); err != nil {
+ return err
+ }
+ return filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ rel, err := filepath.Rel(srcDir, path)
+ if err != nil {
+ return err
+ }
+ if rel == "." {
+ return nil
+ }
+ out := filepath.Join(dest, rel)
+ if d.IsDir() {
+ return os.MkdirAll(out, 0755)
+ }
+ src, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer src.Close()
+ outFile, err := os.Create(out)
+ if err != nil {
+ return err
+ }
+ defer outFile.Close()
+ _, err = io.Copy(outFile, src)
+ return err
+ })
+}
+
+func ManagedFiles(efs fs.FS, payloadRoot string) ([]string, error) {
+ var files []string
+ err := fs.WalkDir(efs, payloadRoot, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ return nil
+ }
+ rel, err := filepath.Rel(payloadRoot, path)
+ if err != nil {
+ return err
+ }
+ files = append(files, filepath.ToSlash(rel))
+ return nil
+ })
+ return files, err
+}
+
+func ManagedFilesFromDir(srcDir string) ([]string, error) {
+ var files []string
+ err := filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ return nil
+ }
+ rel, err := filepath.Rel(srcDir, path)
+ if err != nil {
+ return err
+ }
+ files = append(files, filepath.ToSlash(rel))
+ return nil
+ })
+ return files, err
+}
+
+func ExistsEmbedded(efs fs.FS, payloadRoot, rel string) bool {
+ _, err := efs.Open(filepath.ToSlash(filepath.Join(payloadRoot, rel)))
+ return err == nil
+}
+
+func Exists(path string) bool {
+ _, err := os.Stat(path)
+ return err == nil
+}
+
+func InstallSkill(efs fs.FS, skillRoot, dest, name, displayName, cliVersion, gitCommit, source string) error {
+ if err := CopyPayload(efs, skillRoot, dest); err != nil {
+ return err
+ }
+ files, err := ManagedFiles(efs, skillRoot)
+ if err != nil {
+ return err
+ }
+ return manifest.Write(dest, name, displayName, cliVersion, gitCommit, source, files)
+}
+
+func InstallSkillFromDir(srcDir, dest, name, displayName, cliVersion, gitCommit, source string) error {
+ if err := CopyDir(srcDir, dest); err != nil {
+ return err
+ }
+ files, err := ManagedFilesFromDir(srcDir)
+ if err != nil {
+ return err
+ }
+ return manifest.Write(dest, name, displayName, cliVersion, gitCommit, source, files)
+}
+
+func RemoveSkill(dest string) error {
+ if !Exists(dest) {
+ return fmt.Errorf("not installed at %s", dest)
+ }
+ if !manifest.IsManaged(dest) {
+ return fmt.Errorf("unmanaged install at %s; use --force to remove", dest)
+ }
+ return os.RemoveAll(dest)
+}
diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go
new file mode 100644
index 0000000..5494a2d
--- /dev/null
+++ b/internal/manifest/manifest.go
@@ -0,0 +1,67 @@
+package manifest
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+const ManifestFile = ".composekit-manifest.json"
+
+type Manifest struct {
+ Name string `json:"name"`
+ DisplayName string `json:"displayName"`
+ InstalledBy string `json:"installedBy"`
+ SkillVersion string `json:"skillVersion"`
+ CliVersion string `json:"cliVersion"`
+ GitCommit string `json:"gitCommit"`
+ Source string `json:"source"`
+ InstalledAt string `json:"installedAt"`
+ ManagedFiles []string `json:"managedFiles"`
+}
+
+func Read(skillDir string) (*Manifest, error) {
+ p := filepath.Join(skillDir, ManifestFile)
+ data, err := os.ReadFile(p)
+ if err != nil {
+ return nil, err
+ }
+ var m Manifest
+ if err := json.Unmarshal(data, &m); err != nil {
+ return nil, err
+ }
+ return &m, nil
+}
+
+func Write(skillDir, name, displayName, cliVersion, gitCommit, source string, managedFiles []string) error {
+ m := Manifest{
+ Name: name,
+ DisplayName: displayName,
+ InstalledBy: "composekit",
+ SkillVersion: cliVersion,
+ CliVersion: cliVersion,
+ GitCommit: gitCommit,
+ Source: source,
+ InstalledAt: time.Now().UTC().Format(time.RFC3339),
+ ManagedFiles: managedFiles,
+ }
+ data, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(filepath.Join(skillDir, ManifestFile), data, 0644)
+}
+
+func Exists(skillDir string) bool {
+ _, err := os.Stat(filepath.Join(skillDir, ManifestFile))
+ return err == nil
+}
+
+func IsManaged(skillDir string) bool {
+ m, err := Read(skillDir)
+ if err != nil {
+ return false
+ }
+ return m.InstalledBy == "composekit"
+}
diff --git a/internal/targets/targets.go b/internal/targets/targets.go
new file mode 100644
index 0000000..2bb709f
--- /dev/null
+++ b/internal/targets/targets.go
@@ -0,0 +1,167 @@
+package targets
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+)
+
+type AgentTarget struct {
+ Name string
+ SkillDir string
+ DetectPath string
+}
+
+var KnownAgentTargets = []AgentTarget{
+ {Name: "antigravity", SkillDir: ".gemini/antigravity/skills", DetectPath: ".gemini/antigravity"},
+ {Name: "claude", SkillDir: ".claude/skills", DetectPath: ".claude"},
+ {Name: "codex", SkillDir: ".codex/skills", DetectPath: ".codex"},
+ {Name: "cursor", SkillDir: ".cursor/skills", DetectPath: ".cursor"},
+ {Name: "firebender", SkillDir: ".firebender/skills", DetectPath: ".firebender"},
+ {Name: "gemini", SkillDir: ".gemini/skills", DetectPath: ".gemini"},
+ {Name: "opencode", SkillDir: ".config/opencode/skills", DetectPath: ".config/opencode"},
+}
+
+func HomeDir() string {
+ if v := os.Getenv("COMPOSEKIT_HOME"); v != "" {
+ return v
+ }
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "."
+ }
+ return home
+}
+
+func ExpandHome(path string) string {
+ if strings.HasPrefix(path, "~/") {
+ return filepath.Join(HomeDir(), path[2:])
+ }
+ return path
+}
+
+func AgentSkillDir(t AgentTarget) string {
+ return filepath.Join(HomeDir(), t.SkillDir)
+}
+
+func Detect() []AgentTarget {
+ var detected []AgentTarget
+ for _, t := range KnownAgentTargets {
+ p := ExpandHome(filepath.Join("~", t.DetectPath))
+ if _, err := os.Stat(p); err == nil {
+ detected = append(detected, t)
+ }
+ }
+ return detected
+}
+
+func ResolveTargets(agents []string, allAgents bool, customTargets []string) []string {
+ var dirs []string
+ seen := make(map[string]bool)
+
+ if allAgents {
+ for _, t := range KnownAgentTargets {
+ d := AgentSkillDir(t)
+ if !seen[d] {
+ dirs = append(dirs, d)
+ seen[d] = true
+ }
+ }
+ }
+
+ if len(agents) > 0 {
+ agentSet := make(map[string]bool)
+ for _, a := range agents {
+ agentSet[strings.ToLower(strings.TrimSpace(a))] = true
+ }
+ for _, t := range KnownAgentTargets {
+ if agentSet[t.Name] {
+ d := AgentSkillDir(t)
+ if !seen[d] {
+ dirs = append(dirs, d)
+ seen[d] = true
+ }
+ }
+ }
+ }
+
+ if len(agents) == 0 && !allAgents {
+ detected := Detect()
+ for _, t := range detected {
+ d := AgentSkillDir(t)
+ if !seen[d] {
+ dirs = append(dirs, d)
+ seen[d] = true
+ }
+ }
+ }
+
+ for _, t := range customTargets {
+ if !seen[t] {
+ dirs = append(dirs, t)
+ seen[t] = true
+ }
+ }
+
+ return dirs
+}
+
+func DefaultTargets() []string {
+ return []string{
+ filepath.Join(HomeDir(), ".gemini/antigravity/skills"),
+ filepath.Join(HomeDir(), ".gemini/skills"),
+ }
+}
+
+type DetectResult struct {
+ Name string `json:"name"`
+ Path string `json:"path"`
+ Exists bool `json:"exists"`
+}
+
+func DetectAll() []DetectResult {
+ var results []DetectResult
+ for _, t := range KnownAgentTargets {
+ p := ExpandHome(filepath.Join("~", t.DetectPath))
+ _, err := os.Stat(p)
+ results = append(results, DetectResult{
+ Name: t.Name,
+ Path: AgentSkillDir(t),
+ Exists: err == nil,
+ })
+ }
+ sort.Slice(results, func(i, j int) bool {
+ return results[i].Name < results[j].Name
+ })
+ return results
+}
+
+func AgentNameForPath(path string) string {
+ for _, t := range KnownAgentTargets {
+ expected := AgentSkillDir(t)
+ if expected == path {
+ return t.Name
+ }
+ }
+ return filepath.Base(path)
+}
+
+func PrintDetect(results []DetectResult) {
+ fmt.Println("Detected agent targets:")
+ fmt.Println()
+ for _, r := range results {
+ if r.Exists {
+ fmt.Printf(" %-12s %s\n", r.Name, r.Path)
+ }
+ }
+ fmt.Println()
+ fmt.Println("Not detected:")
+ fmt.Println()
+ for _, r := range results {
+ if !r.Exists {
+ fmt.Printf(" %-12s %s\n", r.Name, r.Path)
+ }
+ }
+}
diff --git a/internal/ui/output.go b/internal/ui/output.go
new file mode 100644
index 0000000..0b2c125
--- /dev/null
+++ b/internal/ui/output.go
@@ -0,0 +1,63 @@
+package ui
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+)
+
+func HasFlag(flag string) bool {
+ for _, arg := range os.Args {
+ if arg == flag {
+ return true
+ }
+ }
+ return false
+}
+
+func GetFlagValue(flag string) string {
+ for i, arg := range os.Args {
+ if arg == flag && i+1 < len(os.Args) {
+ return os.Args[i+1]
+ }
+ }
+ return ""
+}
+
+func GetAgents() []string {
+ val := GetFlagValue("--agent")
+ if val == "" {
+ return nil
+ }
+ return splitCommas(val)
+}
+
+func splitCommas(s string) []string {
+ if s == "" {
+ return nil
+ }
+ var result []string
+ for _, part := range strings.Split(s, ",") {
+ trimmed := strings.TrimSpace(part)
+ if trimmed != "" {
+ result = append(result, trimmed)
+ }
+ }
+ return result
+}
+
+func PrintJSON(v interface{}) {
+ data, err := json.MarshalIndent(v, "", " ")
+ if err != nil {
+ return
+ }
+ fmt.Println(string(data))
+}
+
+func Must(err error) {
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Error:", err)
+ os.Exit(1)
+ }
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..3829cfb
--- /dev/null
+++ b/main.go
@@ -0,0 +1,649 @@
+package main
+
+import (
+ "embed"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/Meet-Miyani/composekit/internal/catalog"
+ "github.com/Meet-Miyani/composekit/internal/config"
+ gh "github.com/Meet-Miyani/composekit/internal/github"
+ "github.com/Meet-Miyani/composekit/internal/install"
+ "github.com/Meet-Miyani/composekit/internal/manifest"
+ "github.com/Meet-Miyani/composekit/internal/targets"
+ "github.com/Meet-Miyani/composekit/internal/ui"
+)
+
+//go:embed skills/** catalog/**
+var embeddedFiles embed.FS
+
+var (
+ version = "dev"
+ commit = "unknown"
+ date = "unknown"
+)
+
+const defaultSkillName = "compose"
+const catalogRoot = "catalog/skills.json"
+
+func main() {
+ if len(os.Args) < 2 {
+ help()
+ return
+ }
+
+ switch os.Args[1] {
+ case "init":
+ ui.Must(cmdInit())
+ case "update":
+ ui.Must(cmdUpdate())
+ case "doctor":
+ ui.Must(cmdDoctor())
+ case "remove":
+ ui.Must(cmdRemove())
+ case "version":
+ fmt.Printf("composekit %s\ncommit: %s\nbuilt: %s\n", version, commit, date)
+ case "skills":
+ if len(os.Args) < 3 {
+ help()
+ return
+ }
+ ui.Must(cmdSkills(os.Args[2]))
+ case "targets":
+ if len(os.Args) < 3 {
+ help()
+ return
+ }
+ ui.Must(cmdTargets(os.Args[2]))
+ case "help", "--help", "-h":
+ help()
+ default:
+ fmt.Println("Unknown command:", os.Args[1])
+ help()
+ os.Exit(1)
+ }
+}
+
+func resolveTargets() []string {
+ if dir := ui.GetFlagValue("--target"); dir != "" {
+ abs, err := filepath.Abs(dir)
+ ui.Must(err)
+ return []string{abs}
+ }
+
+ if ui.HasFlag("--all-agents") {
+ cfg := config.Load()
+ return targets.ResolveTargets(nil, true, cfg.Targets)
+ }
+
+ if agents := ui.GetAgents(); len(agents) > 0 {
+ cfg := config.Load()
+ return targets.ResolveTargets(agents, false, cfg.Targets)
+ }
+
+ cfg := config.Load()
+ result := targets.ResolveTargets(nil, false, cfg.Targets)
+ if len(result) > 0 {
+ return result
+ }
+
+ return targets.DefaultTargets()
+}
+
+func loadCatalog() (*catalog.Catalog, error) {
+ return catalog.Load(embeddedFiles, catalogRoot)
+}
+
+func installFromEmbedded(skill *catalog.SkillEntry, dest string, version, commit, source string) error {
+ return install.InstallSkill(embeddedFiles, skill.Path, dest, skill.Name, skill.DisplayName, version, commit, source)
+}
+
+func installFromDir(skillDir, dest, name, displayName, version, commit, source string) error {
+ return install.InstallSkillFromDir(skillDir, dest, name, displayName, version, commit, source)
+}
+
+func cmdInit() error {
+ cat, err := loadCatalog()
+ if err != nil {
+ return fmt.Errorf("failed to load catalog: %w", err)
+ }
+
+ skill := cat.FindByName(defaultSkillName)
+ if skill == nil {
+ return fmt.Errorf("skill '%s' not found in catalog", defaultSkillName)
+ }
+
+ targetDirs := resolveTargets()
+ force := ui.HasFlag("--force")
+ dryRun := ui.HasFlag("--dry-run")
+ useJSON := ui.HasFlag("--json")
+
+ type result struct {
+ Target string `json:"target"`
+ Status string `json:"status"`
+ }
+ var results []result
+
+ for _, target := range targetDirs {
+ dest := filepath.Join(target, skill.Name)
+ if install.Exists(dest) {
+ if !manifest.IsManaged(dest) && !force {
+ errMsg := fmt.Sprintf("unmanaged install at %s; use --force to overwrite", dest)
+ if useJSON {
+ results = append(results, result{Target: dest, Status: "error: " + errMsg})
+ } else {
+ fmt.Fprintf(os.Stderr, "Error: %s\n", errMsg)
+ }
+ continue
+ }
+ if !dryRun {
+ os.RemoveAll(dest)
+ }
+ }
+ if !dryRun {
+ if err := installFromEmbedded(skill, dest, version, commit, "embedded"); err != nil {
+ return err
+ }
+ }
+ if useJSON {
+ results = append(results, result{Target: dest, Status: "installed"})
+ } else {
+ fmt.Printf("Skill '%s' installed to %s\n", skill.Name, dest)
+ }
+ }
+
+ if dryRun && !useJSON {
+ fmt.Println("Dry run β no changes made")
+ for _, target := range targetDirs {
+ dest := filepath.Join(target, skill.Name)
+ fmt.Printf("Would install '%s' to %s\n", skill.Name, dest)
+ }
+ }
+
+ if useJSON {
+ ui.PrintJSON(results)
+ }
+ return nil
+}
+
+func cmdUpdate() error {
+ offline := ui.HasFlag("--offline")
+ force := ui.HasFlag("--force")
+ dryRun := ui.HasFlag("--dry-run")
+ useJSON := ui.HasFlag("--json")
+
+ var bundleDir string
+ var source string
+ var releaseTag string
+
+ if !offline {
+ fmt.Println("Fetching latest skill bundle from GitHub...")
+ release, err := gh.FetchLatestRelease()
+ if err != nil {
+ return fmt.Errorf("fetch release: %w", err)
+ }
+ releaseTag = release.TagName
+ source = "github-release:" + releaseTag
+
+ tmpDir, err := os.MkdirTemp("", "composekit-update-*")
+ if err != nil {
+ return err
+ }
+ defer os.RemoveAll(tmpDir)
+
+ bundleDir, err = gh.DownloadSkillsBundle(release, tmpDir)
+ if err != nil {
+ return fmt.Errorf("download skills bundle: %w", err)
+ }
+ fmt.Printf("Fetched latest skill bundle: %s\n", releaseTag)
+ } else {
+ source = "embedded"
+ }
+
+ targetDirs := resolveTargets()
+
+ type result struct {
+ Target string `json:"target"`
+ Status string `json:"status"`
+ }
+ var results []result
+
+ for _, target := range targetDirs {
+ dest := filepath.Join(target, defaultSkillName)
+ if !install.Exists(dest) {
+ errMsg := fmt.Sprintf("not installed at %s; run init first", dest)
+ if useJSON {
+ results = append(results, result{Target: dest, Status: "error: " + errMsg})
+ } else {
+ fmt.Fprintf(os.Stderr, "Error: %s\n", errMsg)
+ }
+ continue
+ }
+ manifestPath := filepath.Join(dest, manifest.ManifestFile)
+ if !install.Exists(manifestPath) && !force {
+ errMsg := fmt.Sprintf("manifest missing at %s; use --force to replace unmanaged install", manifestPath)
+ if useJSON {
+ results = append(results, result{Target: dest, Status: "error: " + errMsg})
+ } else {
+ fmt.Fprintf(os.Stderr, "Error: %s\n", errMsg)
+ }
+ continue
+ }
+ if !dryRun {
+ os.RemoveAll(dest)
+ if offline {
+ cat, err := loadCatalog()
+ if err != nil {
+ return err
+ }
+ skill := cat.FindByName(defaultSkillName)
+ if skill == nil {
+ return fmt.Errorf("skill '%s' not found in catalog", defaultSkillName)
+ }
+ if err := installFromEmbedded(skill, dest, version, commit, source); err != nil {
+ return err
+ }
+ } else {
+ skillDir := filepath.Join(bundleDir, "skills", defaultSkillName)
+ if !install.Exists(skillDir) {
+ return fmt.Errorf("skill '%s' not found in downloaded bundle at %s", defaultSkillName, skillDir)
+ }
+ if err := installFromDir(skillDir, dest, defaultSkillName, "Compose Multiplatform", version, commit, source); err != nil {
+ return err
+ }
+ }
+ }
+ if useJSON {
+ results = append(results, result{Target: dest, Status: "updated"})
+ } else {
+ fmt.Printf("Skill '%s' updated at %s\n", defaultSkillName, dest)
+ }
+ }
+
+ if useJSON {
+ ui.PrintJSON(results)
+ }
+ return nil
+}
+
+func cmdRemove() error {
+ targetDirs := resolveTargets()
+ force := ui.HasFlag("--force")
+ dryRun := ui.HasFlag("--dry-run")
+ useJSON := ui.HasFlag("--json")
+
+ type result struct {
+ Target string `json:"target"`
+ Status string `json:"status"`
+ }
+ var results []result
+
+ for _, target := range targetDirs {
+ dest := filepath.Join(target, defaultSkillName)
+ if !install.Exists(dest) {
+ if useJSON {
+ results = append(results, result{Target: dest, Status: "not_installed"})
+ } else {
+ fmt.Println("Not installed:", dest)
+ }
+ continue
+ }
+ if !manifest.IsManaged(dest) && !force {
+ errMsg := fmt.Sprintf("unmanaged install at %s; use --force to remove", dest)
+ if useJSON {
+ results = append(results, result{Target: dest, Status: "error: " + errMsg})
+ } else {
+ fmt.Fprintf(os.Stderr, "Error: %s\n", errMsg)
+ }
+ continue
+ }
+ if !dryRun {
+ if err := os.RemoveAll(dest); err != nil {
+ return err
+ }
+ }
+ if useJSON {
+ results = append(results, result{Target: dest, Status: "removed"})
+ } else {
+ fmt.Println("Removed", dest)
+ }
+ }
+
+ if useJSON {
+ ui.PrintJSON(results)
+ }
+ return nil
+}
+
+func cmdDoctor() error {
+ useJSON := ui.HasFlag("--json")
+
+ cat, err := loadCatalog()
+ if err != nil {
+ return fmt.Errorf("failed to load catalog: %w", err)
+ }
+
+ skill := cat.FindByName(defaultSkillName)
+
+ var skillRoot string
+ if skill != nil {
+ skillRoot = skill.Path
+ }
+
+ refCount := 0
+ if skillRoot != "" {
+ files, _ := install.ManagedFiles(embeddedFiles, skillRoot)
+ for _, f := range files {
+ if strings.HasSuffix(f, ".md") && strings.Contains(f, "references/") {
+ refCount++
+ }
+ }
+ }
+
+ targetDirs := resolveTargets()
+
+ type targetStatus struct {
+ Path string `json:"path"`
+ Installed bool `json:"installed"`
+ Manifest bool `json:"manifest"`
+ Managed bool `json:"managed"`
+ }
+
+ if useJSON {
+ type doctorResult struct {
+ CLI struct {
+ Version string `json:"version"`
+ Commit string `json:"commit"`
+ Date string `json:"date"`
+ } `json:"cli"`
+ Catalog struct {
+ Valid bool `json:"valid"`
+ Skills int `json:"skills"`
+ } `json:"catalog"`
+ Payload struct {
+ Skill string `json:"skill"`
+ Embedded bool `json:"embedded"`
+ Files struct {
+ SkillMD bool `json:"SKILL.md"`
+ AgentYaml bool `json:"agents/openai.yaml"`
+ References int `json:"references"`
+ } `json:"files"`
+ } `json:"payload"`
+ Targets []targetStatus `json:"targets"`
+ }
+
+ var result doctorResult
+ result.CLI.Version = version
+ result.CLI.Commit = commit
+ result.CLI.Date = date
+ result.Catalog.Valid = skill != nil
+ if cat != nil {
+ result.Catalog.Skills = len(cat.Skills)
+ }
+ result.Payload.Skill = defaultSkillName
+ result.Payload.Embedded = true
+ result.Payload.Files.SkillMD = skillRoot != "" && install.ExistsEmbedded(embeddedFiles, skillRoot, "SKILL.md")
+ result.Payload.Files.AgentYaml = skillRoot != "" && install.ExistsEmbedded(embeddedFiles, skillRoot, "agents/openai.yaml")
+ result.Payload.Files.References = refCount
+ for _, target := range targetDirs {
+ dest := filepath.Join(target, defaultSkillName)
+ result.Targets = append(result.Targets, targetStatus{
+ Path: target,
+ Installed: install.Exists(dest),
+ Manifest: install.Exists(filepath.Join(dest, manifest.ManifestFile)),
+ Managed: manifest.IsManaged(dest),
+ })
+ }
+ ui.PrintJSON(result)
+ return nil
+ }
+
+ fmt.Println("ComposeKit doctor")
+ fmt.Println()
+ fmt.Println("CLI:")
+ fmt.Println(" version:", version)
+ fmt.Println(" commit:", commit)
+ fmt.Println(" date:", date)
+ fmt.Println()
+ fmt.Println("Catalog:")
+ fmt.Println(" valid:", skill != nil)
+ if cat != nil {
+ fmt.Println(" skills:", len(cat.Skills))
+ }
+ fmt.Println()
+ fmt.Println("Payload:")
+ fmt.Println(" skill:", defaultSkillName)
+ fmt.Println(" embedded: yes")
+ if skillRoot != "" {
+ fmt.Println(" SKILL.md:", yesno(install.ExistsEmbedded(embeddedFiles, skillRoot, "SKILL.md")))
+ fmt.Println(" agents/openai.yaml:", yesno(install.ExistsEmbedded(embeddedFiles, skillRoot, "agents/openai.yaml")))
+ }
+ fmt.Println(" references:", refCount, "files")
+ fmt.Println()
+ fmt.Println("Targets:")
+ for _, target := range targetDirs {
+ dest := filepath.Join(target, defaultSkillName)
+ fmt.Printf(" %s\n", target)
+ fmt.Printf(" installed: %s\n", yesno(install.Exists(dest)))
+ fmt.Printf(" manifest: %s\n", yesno(install.Exists(filepath.Join(dest, manifest.ManifestFile))))
+ fmt.Printf(" managed: %s\n", yesno(manifest.IsManaged(dest)))
+ }
+ return nil
+}
+
+func cmdSkills(subcommand string) error {
+ cat, err := loadCatalog()
+ if err != nil {
+ return fmt.Errorf("failed to load catalog: %w", err)
+ }
+
+ switch subcommand {
+ case "list":
+ if ui.HasFlag("--long") || ui.HasFlag("-l") {
+ targetDirs := resolveTargets()
+ statuses := make(map[string][]catalog.InstallStatus)
+ for _, s := range cat.Skills {
+ var sts []catalog.InstallStatus
+ for _, target := range targetDirs {
+ dest := filepath.Join(target, s.Name)
+ sts = append(sts, catalog.InstallStatus{
+ Name: target,
+ Installed: install.Exists(dest),
+ Path: dest,
+ })
+ }
+ statuses[s.Name] = sts
+ }
+ cat.PrintSkillsLong(statuses)
+ } else {
+ cat.PrintListShort()
+ }
+
+ case "find":
+ if len(os.Args) < 4 {
+ return fmt.Errorf("usage: composekit skills find ")
+ }
+ query := os.Args[3]
+ results := cat.FindByQuery(query)
+ cat.PrintFindResults(query, results)
+
+ case "add":
+ if len(os.Args) < 4 {
+ return fmt.Errorf("usage: composekit skills add ")
+ }
+ skillToAdd := os.Args[3]
+ s := cat.FindByName(skillToAdd)
+ if s == nil {
+ return fmt.Errorf("skill '%s' not found in catalog", skillToAdd)
+ }
+
+ force := ui.HasFlag("--force")
+ targetDirs := resolveTargets()
+ for _, target := range targetDirs {
+ dest := filepath.Join(target, s.Name)
+ if install.Exists(dest) {
+ if !manifest.IsManaged(dest) && !force {
+ return fmt.Errorf("unmanaged install at %s; use --force to overwrite", dest)
+ }
+ os.RemoveAll(dest)
+ }
+ if err := installFromEmbedded(s, dest, version, commit, "embedded"); err != nil {
+ return err
+ }
+ fmt.Printf("Skill '%s' installed to %s\n", s.Name, dest)
+ }
+
+ case "remove":
+ if len(os.Args) < 4 {
+ return fmt.Errorf("usage: composekit skills remove ")
+ }
+ skillToRemove := os.Args[3]
+ force := ui.HasFlag("--force")
+ targetDirs := resolveTargets()
+ for _, target := range targetDirs {
+ dest := filepath.Join(target, skillToRemove)
+ if !install.Exists(dest) {
+ fmt.Println("Not installed:", dest)
+ continue
+ }
+ if !manifest.IsManaged(dest) && !force {
+ return fmt.Errorf("unmanaged install at %s; use --force to remove", dest)
+ }
+ if err := os.RemoveAll(dest); err != nil {
+ return err
+ }
+ fmt.Println("Removed", dest)
+ }
+
+ case "installed":
+ targetDirs := resolveTargets()
+ fmt.Println("Installed skills:")
+ for _, target := range targetDirs {
+ for _, s := range cat.Skills {
+ dest := filepath.Join(target, s.Name)
+ if install.Exists(dest) {
+ fmt.Printf(" %s at %s\n", s.Name, dest)
+ }
+ }
+ }
+
+ default:
+ return fmt.Errorf("unknown skills subcommand: %s", subcommand)
+ }
+ return nil
+}
+
+func cmdTargets(subcommand string) error {
+ switch subcommand {
+ case "detect":
+ results := targets.DetectAll()
+ targets.PrintDetect(results)
+
+ case "list":
+ cfg := config.Load()
+ if len(cfg.Targets) == 0 {
+ fmt.Println("No targets configured")
+ return nil
+ }
+ fmt.Println("Configured targets:")
+ for _, t := range cfg.Targets {
+ dest := filepath.Join(t, defaultSkillName)
+ status := "not installed"
+ if install.Exists(dest) {
+ status = "installed"
+ }
+ fmt.Printf(" %s (%s)\n", t, status)
+ }
+
+ case "add":
+ if len(os.Args) < 4 {
+ return fmt.Errorf("usage: composekit targets add ")
+ }
+ dir := os.Args[3]
+ abs, err := filepath.Abs(dir)
+ if err != nil {
+ return err
+ }
+ cfg := config.Load()
+ for _, t := range cfg.Targets {
+ if t == abs {
+ fmt.Println("Target already exists:", abs)
+ return nil
+ }
+ }
+ cfg.Targets = append(cfg.Targets, abs)
+ if err := config.Save(cfg); err != nil {
+ return err
+ }
+ fmt.Println("Added target:", abs)
+
+ case "remove":
+ if len(os.Args) < 4 {
+ return fmt.Errorf("usage: composekit targets remove ")
+ }
+ dir := os.Args[3]
+ abs, err := filepath.Abs(dir)
+ if err != nil {
+ return err
+ }
+ cfg := config.Load()
+ var updated []string
+ found := false
+ for _, t := range cfg.Targets {
+ if t == abs {
+ found = true
+ continue
+ }
+ updated = append(updated, t)
+ }
+ if !found {
+ return fmt.Errorf("target not found: %s", abs)
+ }
+ cfg.Targets = updated
+ if err := config.Save(cfg); err != nil {
+ return err
+ }
+ fmt.Println("Removed target:", abs)
+
+ default:
+ return fmt.Errorf("unknown targets subcommand: %s", subcommand)
+ }
+ return nil
+}
+
+func yesno(b bool) string {
+ if b {
+ return "yes"
+ }
+ return "no"
+}
+
+func help() {
+ fmt.Println(strings.TrimSpace(`ComposeKit
+
+A CLI tool to install and manage AI coding skills for Jetpack Compose,
+Compose Multiplatform, and Kotlin Multiplatform workflows.
+
+Usage:
+ composekit init [--target ] [--agent ] [--all-agents] [--force] [--dry-run] [--json]
+ composekit update [--target ] [--agent ] [--all-agents] [--force] [--dry-run] [--json] [--offline]
+ composekit doctor [--target ] [--json]
+ composekit remove [--target ] [--agent ] [--all-agents] [--force] [--dry-run] [--json]
+ composekit version
+
+Skills:
+ composekit skills list [--long]
+ composekit skills find
+ composekit skills add [--target ] [--agent ] [--all-agents] [--force]
+ composekit skills remove [--target ] [--agent ] [--all-agents] [--force]
+ composekit skills installed
+
+Targets:
+ composekit targets detect
+ composekit targets list
+ composekit targets add
+ composekit targets remove
+`))
+}
diff --git a/main_test.go b/main_test.go
new file mode 100644
index 0000000..4267716
--- /dev/null
+++ b/main_test.go
@@ -0,0 +1,146 @@
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/Meet-Miyani/composekit/internal/catalog"
+ "github.com/Meet-Miyani/composekit/internal/install"
+ "github.com/Meet-Miyani/composekit/internal/manifest"
+)
+
+func TestManagedFiles(t *testing.T) {
+ cat, err := loadCatalog()
+ if err != nil {
+ t.Fatal(err)
+ }
+ skill := cat.FindByName(defaultSkillName)
+ if skill == nil {
+ t.Fatalf("skill %q not found", defaultSkillName)
+ }
+
+ files, err := install.ManagedFiles(embeddedFiles, skill.Path)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(files) == 0 {
+ t.Fatal("expected at least one managed file")
+ }
+
+ found := false
+ for _, f := range files {
+ if f == "SKILL.md" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("expected SKILL.md in managed files")
+ }
+
+ found = false
+ for _, f := range files {
+ if f == "agents/openai.yaml" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("expected agents/openai.yaml in managed files")
+ }
+}
+
+func TestExistsEmbedded(t *testing.T) {
+ cat, err := loadCatalog()
+ if err != nil {
+ t.Fatal(err)
+ }
+ skill := cat.FindByName(defaultSkillName)
+ if skill == nil {
+ t.Fatalf("skill %q not found", defaultSkillName)
+ }
+
+ if !install.ExistsEmbedded(embeddedFiles, skill.Path, "SKILL.md") {
+ t.Error("expected SKILL.md to exist in embedded payload")
+ }
+ if !install.ExistsEmbedded(embeddedFiles, skill.Path, "agents/openai.yaml") {
+ t.Error("expected agents/openai.yaml to exist in embedded payload")
+ }
+ if install.ExistsEmbedded(embeddedFiles, skill.Path, "nonexistent.md") {
+ t.Error("expected nonexistent.md to not exist")
+ }
+}
+
+func TestInitAndRemove(t *testing.T) {
+ tmpDir := t.TempDir()
+ targetDir := filepath.Join(tmpDir, "skills")
+
+ oldArgs := os.Args
+ defer func() { os.Args = oldArgs }()
+
+ t.Setenv("COMPOSEKIT_CONFIG_HOME", filepath.Join(tmpDir, "config"))
+ os.Args = []string{"composekit", "init", "--target", targetDir}
+ if err := cmdInit(); err != nil {
+ t.Fatal(err)
+ }
+
+ dest := filepath.Join(targetDir, defaultSkillName)
+ if !install.Exists(dest) {
+ t.Fatal("expected skill to be installed")
+ }
+
+ if !install.Exists(filepath.Join(dest, "SKILL.md")) {
+ t.Error("expected SKILL.md to exist")
+ }
+
+ if !install.Exists(filepath.Join(dest, manifest.ManifestFile)) {
+ t.Error("expected manifest to exist")
+ }
+
+ // Test update (offline mode)
+ os.Args = []string{"composekit", "update", "--target", targetDir, "--force", "--offline"}
+ if err := cmdUpdate(); err != nil {
+ t.Fatal(err)
+ }
+
+ if !install.Exists(dest) {
+ t.Fatal("expected skill to still exist after update")
+ }
+
+ // Test remove
+ os.Args = []string{"composekit", "remove", "--target", targetDir}
+ if err := cmdRemove(); err != nil {
+ t.Fatal(err)
+ }
+
+ if install.Exists(dest) {
+ t.Error("expected skill to be removed")
+ }
+}
+
+func TestCatalog(t *testing.T) {
+ cat, err := catalog.Load(embeddedFiles, catalogRoot)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(cat.Skills) == 0 {
+ t.Fatal("expected at least one skill in catalog")
+ }
+}
+
+func TestCatalogFind(t *testing.T) {
+ cat, err := catalog.Load(embeddedFiles, catalogRoot)
+ if err != nil {
+ t.Fatal(err)
+ }
+ s := cat.FindByName("compose")
+ if s == nil {
+ t.Fatal("expected to find compose skill")
+ }
+ results := cat.FindByQuery("navigation")
+ if len(results) == 0 {
+ t.Error("expected to find skills matching 'navigation'")
+ }
+}
diff --git a/scripts/package-skills.sh b/scripts/package-skills.sh
new file mode 100755
index 0000000..46d558d
--- /dev/null
+++ b/scripts/package-skills.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+VERSION="${1:-snapshot}"
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT"
+
+mkdir -p dist
+
+tar -czf "dist/composekit-skills_${VERSION}.tar.gz" \
+ skills/compose/ \
+ catalog/skills.json
+
+echo "Created dist/composekit-skills_${VERSION}.tar.gz"
diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh
new file mode 100755
index 0000000..90aca21
--- /dev/null
+++ b/scripts/smoke-test.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT"
+
+rm -rf ./tmp-smoke
+mkdir -p ./tmp-smoke/bin
+
+go build -o ./tmp-smoke/bin/composekit .
+
+export COMPOSEKIT_HOME="$PWD/tmp-smoke/home"
+export COMPOSEKIT_CONFIG_HOME="$PWD/tmp-smoke/config"
+
+mkdir -p "$COMPOSEKIT_HOME/.codex"
+mkdir -p "$COMPOSEKIT_HOME/.cursor"
+mkdir -p "$COMPOSEKIT_HOME/.claude"
+mkdir -p "$COMPOSEKIT_CONFIG_HOME"
+
+./tmp-smoke/bin/composekit version
+./tmp-smoke/bin/composekit targets detect
+
+./tmp-smoke/bin/composekit skills list
+./tmp-smoke/bin/composekit skills list --long 2>&1 | head -20
+./tmp-smoke/bin/composekit skills find kmp
+
+./tmp-smoke/bin/composekit init
+
+test -f "$COMPOSEKIT_HOME/.codex/skills/compose/SKILL.md"
+test -f "$COMPOSEKIT_HOME/.cursor/skills/compose/SKILL.md"
+test -f "$COMPOSEKIT_HOME/.claude/skills/compose/SKILL.md"
+
+./tmp-smoke/bin/composekit doctor
+
+./tmp-smoke/bin/composekit update --offline
+
+test -f "$COMPOSEKIT_HOME/.codex/skills/compose/.composekit-manifest.json"
+test -f "$COMPOSEKIT_HOME/.cursor/skills/compose/.composekit-manifest.json"
+test -f "$COMPOSEKIT_HOME/.claude/skills/compose/.composekit-manifest.json"
+
+./tmp-smoke/bin/composekit remove
+
+test ! -d "$COMPOSEKIT_HOME/.codex/skills/compose"
+test ! -d "$COMPOSEKIT_HOME/.cursor/skills/compose"
+test ! -d "$COMPOSEKIT_HOME/.claude/skills/compose"
+
+# Test --all-agents flag
+mkdir -p "$COMPOSEKIT_HOME/.gemini"
+./tmp-smoke/bin/composekit init --all-agents
+
+test -f "$COMPOSEKIT_HOME/.codex/skills/compose/SKILL.md"
+test -f "$COMPOSEKIT_HOME/.cursor/skills/compose/SKILL.md"
+test -f "$COMPOSEKIT_HOME/.claude/skills/compose/SKILL.md"
+test -f "$COMPOSEKIT_HOME/.gemini/skills/compose/SKILL.md"
+
+./tmp-smoke/bin/composekit remove --all-agents
+
+test ! -d "$COMPOSEKIT_HOME/.codex/skills/compose"
+test ! -d "$COMPOSEKIT_HOME/.cursor/skills/compose"
+test ! -d "$COMPOSEKIT_HOME/.claude/skills/compose"
+test ! -d "$COMPOSEKIT_HOME/.gemini/skills/compose"
+
+echo "smoke test passed"
diff --git a/scripts/validate.sh b/scripts/validate-skill.sh
similarity index 99%
rename from scripts/validate.sh
rename to scripts/validate-skill.sh
index 4576158..ef2022b 100755
--- a/scripts/validate.sh
+++ b/scripts/validate-skill.sh
@@ -1,6 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT/skills/compose"
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β Agent Skill Scanner v4 β
# β Validates skill packages against the agentskills.io spec β
@@ -21,8 +24,8 @@ set -euo pipefail
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#
# Usage:
-# ./scripts/validate.sh # full scan
-# ./scripts/validate.sh --help # show usage
+# ./scripts/validate-skill.sh # full scan
+# ./scripts/validate-skill.sh --help # show usage
#
# Environment:
# CI=true β emits GitHub Actions annotations (auto-detected)
@@ -39,6 +42,7 @@ CURRENT_SECTION=""
CI="${CI:-false}"
NO_COLOR="${NO_COLOR:-0}"
OUTPUT_MODE="terminal"
+QUICK_MODE=false
SKILL_FILE="SKILL.md"
SKILL_DIR="$(basename "$(pwd)")"
@@ -2061,11 +2065,11 @@ show_help() {
echo " Computes a quality score (0-100) across 5 dimensions."
echo ""
echo " $(_bold "Usage:")"
- echo " ./scripts/validate.sh Run full scan (terminal)"
- echo " ./scripts/validate.sh --json Output as JSON"
- echo " ./scripts/validate.sh --md Output as Markdown report"
- echo " ./scripts/validate.sh --score-only Print score and grade only"
- echo " ./scripts/validate.sh --help Show this help"
+ echo " ./scripts/validate-skill.sh Run full scan (terminal)"
+ echo " ./scripts/validate-skill.sh --json Output as JSON"
+ echo " ./scripts/validate-skill.sh --md Output as Markdown report"
+ echo " ./scripts/validate-skill.sh --score-only Print score and grade only"
+ echo " ./scripts/validate-skill.sh --help Show this help"
echo ""
echo " $(_bold "Output Modes:")"
echo " $(_cyan "--json") Machine-readable JSON (pipe to jq, feed to web tools)"
@@ -2140,6 +2144,7 @@ main() {
--json) OUTPUT_MODE="json"; NO_COLOR=1 ;;
--md|--markdown) OUTPUT_MODE="markdown"; NO_COLOR=1 ;;
--score-only|--score) OUTPUT_MODE="score-only" ;;
+ --quick) QUICK_MODE=true ;;
*) echo "Unknown flag: $1"; echo "Run with --help for usage."; exit 2 ;;
esac
shift
@@ -2174,16 +2179,18 @@ main() {
check_frontmatter
check_body
check_links
- check_references
+ if [ "$QUICK_MODE" = false ]; then
+ check_references
+ check_reference_depth
+ check_token_budget
+ check_content_quality
+ check_heading_hierarchy
+ fi
check_markdown
- check_reference_depth
check_scripts
check_repo_hygiene
- check_token_budget
- check_content_quality
check_agents_metadata
check_security
- check_heading_hierarchy
compute_quality_score
if [ "$OUTPUT_MODE" != "terminal" ]; then
diff --git a/SKILL.md b/skills/compose/SKILL.md
similarity index 97%
rename from SKILL.md
rename to skills/compose/SKILL.md
index 2116abf..3ab00b2 100644
--- a/SKILL.md
+++ b/skills/compose/SKILL.md
@@ -1,11 +1,11 @@
---
-name: compose-skill
-license: MIT
+name: compose
description: >
- Jetpack Compose and Compose Multiplatform (KMP/CMP) architecture skill.
- Only use when the user explicitly mentions "compose-skill", "@compose-skill",
- or "use compose skill" in their message. Do NOT auto-activate based on
- keyword matching β this skill should only be triggered by direct user request.
+ production-grade jetpack compose and compose multiplatform guidance for kotlin, kmp, cmp,
+ architecture, state management, navigation, networking, persistence, performance,
+ accessibility, testing, previews, and cross-platform ui delivery. use when the user
+ explicitly asks for compose guidance, compose multiplatform help, kotlin multiplatform ui
+ guidance, or requests the compose skill.
---
# Jetpack Compose & Compose Multiplatform
diff --git a/skills/compose/agents/openai.yaml b/skills/compose/agents/openai.yaml
new file mode 100644
index 0000000..f1df86d
--- /dev/null
+++ b/skills/compose/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "Compose Multiplatform"
+ short_description: "Production-grade Jetpack Compose and Compose Multiplatform guidance for Kotlin, KMP, CMP, architecture, state management, navigation, networking, persistence, performance, accessibility, testing, previews, and cross-platform UI delivery."
+ default_prompt: "Use $compose to build, refactor, or review Compose/CMP features, adapting to the project's architecture (MVI recommended for new work), and verifying dependencies before recommending them."
diff --git a/assets/compose-multiplatform-icon.svg b/skills/compose/assets/compose-multiplatform-icon.svg
similarity index 100%
rename from assets/compose-multiplatform-icon.svg
rename to skills/compose/assets/compose-multiplatform-icon.svg
diff --git a/references/accessibility.md b/skills/compose/references/accessibility.md
similarity index 100%
rename from references/accessibility.md
rename to skills/compose/references/accessibility.md
diff --git a/references/animations-advanced.md b/skills/compose/references/animations-advanced.md
similarity index 100%
rename from references/animations-advanced.md
rename to skills/compose/references/animations-advanced.md
diff --git a/references/animations.md b/skills/compose/references/animations.md
similarity index 100%
rename from references/animations.md
rename to skills/compose/references/animations.md
diff --git a/references/anti-patterns.md b/skills/compose/references/anti-patterns.md
similarity index 100%
rename from references/anti-patterns.md
rename to skills/compose/references/anti-patterns.md
diff --git a/references/architecture.md b/skills/compose/references/architecture.md
similarity index 100%
rename from references/architecture.md
rename to skills/compose/references/architecture.md
diff --git a/references/ci-cd-distribution.md b/skills/compose/references/ci-cd-distribution.md
similarity index 100%
rename from references/ci-cd-distribution.md
rename to skills/compose/references/ci-cd-distribution.md
diff --git a/references/clean-code.md b/skills/compose/references/clean-code.md
similarity index 100%
rename from references/clean-code.md
rename to skills/compose/references/clean-code.md
diff --git a/references/compose-essentials.md b/skills/compose/references/compose-essentials.md
similarity index 100%
rename from references/compose-essentials.md
rename to skills/compose/references/compose-essentials.md
diff --git a/references/coroutines-flow-advanced.md b/skills/compose/references/coroutines-flow-advanced.md
similarity index 100%
rename from references/coroutines-flow-advanced.md
rename to skills/compose/references/coroutines-flow-advanced.md
diff --git a/references/coroutines-flow.md b/skills/compose/references/coroutines-flow.md
similarity index 100%
rename from references/coroutines-flow.md
rename to skills/compose/references/coroutines-flow.md
diff --git a/references/cross-platform.md b/skills/compose/references/cross-platform.md
similarity index 100%
rename from references/cross-platform.md
rename to skills/compose/references/cross-platform.md
diff --git a/references/datastore.md b/skills/compose/references/datastore.md
similarity index 100%
rename from references/datastore.md
rename to skills/compose/references/datastore.md
diff --git a/references/dependency-injection.md b/skills/compose/references/dependency-injection.md
similarity index 100%
rename from references/dependency-injection.md
rename to skills/compose/references/dependency-injection.md
diff --git a/references/gradle-build.md b/skills/compose/references/gradle-build.md
similarity index 100%
rename from references/gradle-build.md
rename to skills/compose/references/gradle-build.md
diff --git a/references/hilt.md b/skills/compose/references/hilt.md
similarity index 100%
rename from references/hilt.md
rename to skills/compose/references/hilt.md
diff --git a/references/image-loading.md b/skills/compose/references/image-loading.md
similarity index 100%
rename from references/image-loading.md
rename to skills/compose/references/image-loading.md
diff --git a/references/ios-swift-interop.md b/skills/compose/references/ios-swift-interop.md
similarity index 100%
rename from references/ios-swift-interop.md
rename to skills/compose/references/ios-swift-interop.md
diff --git a/references/koin.md b/skills/compose/references/koin.md
similarity index 100%
rename from references/koin.md
rename to skills/compose/references/koin.md
diff --git a/references/lists-grids.md b/skills/compose/references/lists-grids.md
similarity index 100%
rename from references/lists-grids.md
rename to skills/compose/references/lists-grids.md
diff --git a/references/material-design.md b/skills/compose/references/material-design.md
similarity index 100%
rename from references/material-design.md
rename to skills/compose/references/material-design.md
diff --git a/references/mvi.md b/skills/compose/references/mvi.md
similarity index 100%
rename from references/mvi.md
rename to skills/compose/references/mvi.md
diff --git a/references/mvvm.md b/skills/compose/references/mvvm.md
similarity index 100%
rename from references/mvvm.md
rename to skills/compose/references/mvvm.md
diff --git a/references/navigation-2-di.md b/skills/compose/references/navigation-2-di.md
similarity index 100%
rename from references/navigation-2-di.md
rename to skills/compose/references/navigation-2-di.md
diff --git a/references/navigation-2.md b/skills/compose/references/navigation-2.md
similarity index 100%
rename from references/navigation-2.md
rename to skills/compose/references/navigation-2.md
diff --git a/references/navigation-3-di.md b/skills/compose/references/navigation-3-di.md
similarity index 100%
rename from references/navigation-3-di.md
rename to skills/compose/references/navigation-3-di.md
diff --git a/references/navigation-3.md b/skills/compose/references/navigation-3.md
similarity index 100%
rename from references/navigation-3.md
rename to skills/compose/references/navigation-3.md
diff --git a/references/navigation-migration.md b/skills/compose/references/navigation-migration.md
similarity index 100%
rename from references/navigation-migration.md
rename to skills/compose/references/navigation-migration.md
diff --git a/references/navigation.md b/skills/compose/references/navigation.md
similarity index 100%
rename from references/navigation.md
rename to skills/compose/references/navigation.md
diff --git a/references/networking-ktor-architecture.md b/skills/compose/references/networking-ktor-architecture.md
similarity index 100%
rename from references/networking-ktor-architecture.md
rename to skills/compose/references/networking-ktor-architecture.md
diff --git a/references/networking-ktor-auth.md b/skills/compose/references/networking-ktor-auth.md
similarity index 100%
rename from references/networking-ktor-auth.md
rename to skills/compose/references/networking-ktor-auth.md
diff --git a/references/networking-ktor-testing.md b/skills/compose/references/networking-ktor-testing.md
similarity index 100%
rename from references/networking-ktor-testing.md
rename to skills/compose/references/networking-ktor-testing.md
diff --git a/references/networking-ktor.md b/skills/compose/references/networking-ktor.md
similarity index 100%
rename from references/networking-ktor.md
rename to skills/compose/references/networking-ktor.md
diff --git a/references/paging-mvi-testing.md b/skills/compose/references/paging-mvi-testing.md
similarity index 100%
rename from references/paging-mvi-testing.md
rename to skills/compose/references/paging-mvi-testing.md
diff --git a/references/paging-offline.md b/skills/compose/references/paging-offline.md
similarity index 100%
rename from references/paging-offline.md
rename to skills/compose/references/paging-offline.md
diff --git a/references/paging.md b/skills/compose/references/paging.md
similarity index 100%
rename from references/paging.md
rename to skills/compose/references/paging.md
diff --git a/references/performance.md b/skills/compose/references/performance.md
similarity index 100%
rename from references/performance.md
rename to skills/compose/references/performance.md
diff --git a/references/resources.md b/skills/compose/references/resources.md
similarity index 100%
rename from references/resources.md
rename to skills/compose/references/resources.md
diff --git a/references/room-database.md b/skills/compose/references/room-database.md
similarity index 100%
rename from references/room-database.md
rename to skills/compose/references/room-database.md
diff --git a/references/testing.md b/skills/compose/references/testing.md
similarity index 100%
rename from references/testing.md
rename to skills/compose/references/testing.md
diff --git a/references/ui-ux.md b/skills/compose/references/ui-ux.md
similarity index 100%
rename from references/ui-ux.md
rename to skills/compose/references/ui-ux.md