create-cmp-cli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -27
- package/bin/create-cmp.mjs +2 -1
- package/options.schema.json +5 -2
- package/package.json +2 -2
- package/src/commands/create.mjs +26 -3
- package/src/lib/toggle.mjs +2 -2
- package/template/.claude/settings.json +12 -0
- package/template/.claude/skills/add-feature/SKILL.md +131 -0
- package/template/.claude/skills/add-repository/SKILL.md +106 -0
- package/template/.claude/skills/add-screen/SKILL.md +130 -0
- package/template/.github/workflows/verify.yml +30 -8
- package/template/.gradle/8.11.1/checksums/checksums.lock +0 -0
- package/template/.gradle/8.11.1/fileChanges/last-build.bin +0 -0
- package/template/.gradle/8.11.1/fileHashes/fileHashes.lock +0 -0
- package/template/.gradle/8.11.1/gc.properties +0 -0
- package/template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/template/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/template/.gradle/vcs-1/gc.properties +0 -0
- package/template/CHANGELOG.md +14 -0
- package/template/CLAUDE.md +70 -0
- package/template/CONTRIBUTING.md +39 -0
- package/template/README.md +103 -0
- package/template/composeApp/build.gradle.kts +15 -8
- package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +4 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +10 -2
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +10 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +6 -2
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +1 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt +2 -1
- package/template/composeApp/src/commonTest/kotlin/com/example/app/data/remote/ItemRepositoryImplTest.kt +26 -0
- package/template/composeApp/src/commonTest/kotlin/com/example/app/domain/usecase/GetItemsUseCaseTest.kt +37 -0
- package/template/composeApp/src/commonTest/kotlin/com/example/app/presentation/home/HomeViewModelTest.kt +104 -0
- package/template/composeApp/src/commonTest/kotlin/com/example/app/testing/fakes/FakeItemRepository.kt +29 -0
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/A11yConformanceTest.kt +64 -0
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +143 -0
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/home/HomeGoldenTreeTest.kt +78 -0
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/home/HomeScreenTest.kt +77 -0
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/navigation/AppShellTest.kt +79 -0
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/testing/ComposeTestExt.kt +16 -0
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/testing/StructuralTree.kt +63 -0
- package/template/docs/ARCHITECTURE.md +51 -0
- package/template/docs/TESTING.md +64 -0
- package/template/docs/adr/0001-adopt-the-create-cmp-harness-conventions.md +32 -0
- package/template/docs/adr/template.md +17 -0
- package/template/gitignore +8 -1
- package/template/gradle/libs.versions.toml +2 -0
- package/template/manifest.json +11 -8
- package/template/qa/e2e/README.md +29 -0
- package/template/qa/e2e/smoke.yaml +35 -0
- package/template/qa/evidence/schema.json +56 -0
- package/template/qa/golden/home.json +15 -0
- package/template/qa/lib/inputs-hash.mjs +112 -0
- package/template/qa/lib/token-drift.mjs +94 -0
- package/template/qa/receipt-check.mjs +113 -0
- package/template/qa/refusal-demo.mjs +491 -0
- package/template/qa/scaffold-feature.mjs +488 -0
- package/template/qa/verify.mjs +467 -0
- package/template/specs/README.md +33 -0
- package/template/specs/app-base.spec.md +30 -0
- package/template/specs/home.spec.md +18 -0
- package/template/qa/appium/README.md +0 -23
- package/template/qa/appium/lib/appium-client.mjs +0 -225
- package/template/qa/appium/package.json +0 -8
- package/template/qa/appium/run-android-smoke.mjs +0 -39
- package/template/tests/appium/cmp/conftest.py +0 -96
- package/template/tests/appium/cmp/test_smoke.py +0 -17
package/README.md
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# create-cmp
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**The AI delivery harness for Kotlin/Compose Multiplatform — scaffold a green-building app (Android + iOS) in minutes, with best practices your AI is mechanically held to.**
|
|
6
6
|
|
|
7
|
-
Toolchain auto-bootstrapped · Navigation & insets pre-solved · Clean Architecture wired ·
|
|
7
|
+
Toolchain auto-bootstrapped · Navigation & insets pre-solved · Clean Architecture wired · Test pyramid + E2E flows included · AI-native inspector & verification lane.
|
|
8
8
|
|
|
9
9
|
[](https://github.com/kvdm-co-pilot/create-cmp/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/create-cmp-cli)
|
|
10
11
|
[](./LICENSE)
|
|
11
12
|
[](./CONTRIBUTING.md)
|
|
12
13
|
[](https://kotlinlang.org/docs/multiplatform.html)
|
|
@@ -27,6 +28,14 @@ to React Native.
|
|
|
27
28
|
freehand-generate your project), bootstraps the toolchain, and **proves a green build** before it
|
|
28
29
|
reports success.
|
|
29
30
|
|
|
31
|
+
And it goes further than scaffolding: `create-cmp` is an **AI delivery harness**. In the age of
|
|
32
|
+
AI-driven development, the scarce thing isn't code — it's a machine-enforceable definition of
|
|
33
|
+
*correct*. Every generated project ships the architecture patterns as running exemplars (with
|
|
34
|
+
tests), the best practices as executable conformance checks, generation tools so the AI extends
|
|
35
|
+
the app right-by-construction, an AI-native structural inspector, and a verification lane that
|
|
36
|
+
refuses "done" without evidence. See [`docs/HARNESS-PLAN.md`](./docs/HARNESS-PLAN.md) for the
|
|
37
|
+
full product definition.
|
|
38
|
+
|
|
30
39
|
## Contents
|
|
31
40
|
|
|
32
41
|
- [Quick start](#quick-start)
|
|
@@ -73,7 +82,7 @@ except `create` works on **any** KMP project, not only ones it scaffolded:
|
|
|
73
82
|
| Command | What it does |
|
|
74
83
|
|---|---|
|
|
75
84
|
| `create-cmp [dir]` / `create-cmp create` | Scaffold a new app from the frozen golden template (the default command). |
|
|
76
|
-
| `create-cmp doctor [--fix]` | Toolchain preflight (JDK/SDK/Xcode/Appium, consent-gated installs) **plus** project diagnosis when run inside a Gradle project: kotlin↔ksp lockstep, drift vs proven-green sets, the KSP2/iOS catch-22, sdk.dir, `~/.konan` bloat, disk space. `--fix` applies the safe heals. |
|
|
85
|
+
| `create-cmp doctor [--fix]` | Toolchain preflight (JDK/SDK/Xcode/Appium — legacy driver install for the pre-Maestro path, consent-gated installs) **plus** project diagnosis when run inside a Gradle project: kotlin↔ksp lockstep, drift vs proven-green sets, the KSP2/iOS catch-22, sdk.dir, `~/.konan` bloat, disk space. `--fix` applies the safe heals. |
|
|
77
86
|
| `create-cmp upgrade [--dry-run]` | Migrate `gradle/libs.versions.toml` to the next proven-green version set — diff table first, surgical in-place edits with `.bak-upgrade` backups, kotlin↔ksp lockstep guardrail, `--verify` to prove the result. |
|
|
78
87
|
| `create-cmp clean` | Cache & build-output hygiene: stale `~/.konan` toolchains + project `build/`/`.gradle/` dirs (sizes shown, consent-gated); `~/.gradle/caches` is size-reported only. |
|
|
79
88
|
| `create-cmp verify [--target-dir .]` | Run the green-build gate (Android, and iOS on macOS) against an existing project. |
|
|
@@ -103,10 +112,25 @@ per run, so it can't silently drift:
|
|
|
103
112
|
presentation{components,theme,navigation,<feature>} / di` with Koin modules registered and **one
|
|
104
113
|
example feature wired end-to-end** as the copy-paste pattern.
|
|
105
114
|
- **Theme & tokens** — `<Prefix>Theme`, `<Prefix>Tokens`, `<Prefix>Colors`, DM Sans.
|
|
106
|
-
- **
|
|
107
|
-
|
|
115
|
+
- **The test pyramid, pre-built** — unit exemplars (Turbine + fakes), dependency-free architecture
|
|
116
|
+
conformance gates, Compose UI Tests, golden-tree structural baselines, and Maestro E2E flows —
|
|
117
|
+
bound to a committed behavior spec (`specs/*.spec.md`, Given/When/Then, stable clause ids) so a
|
|
118
|
+
`specCoverage` gate catches untested or untraceable behavior — plus the verify lane
|
|
119
|
+
(`qa/verify.mjs`) that runs it all and emits a typed PASS/FAIL/SKIP verdict + evidence receipt.
|
|
120
|
+
- **Extend it with Claude Code, no plugin required** — every generated project ships its own
|
|
121
|
+
`add-feature`/`add-screen`/`add-repository` skills (backed by the deterministic
|
|
122
|
+
`qa/scaffold-feature.mjs` stamper) that clone the exemplar feature end-to-end: Screen →
|
|
123
|
+
ViewModel → UseCase → Repository → DI → nav, tests at every layer, golden baseline included.
|
|
124
|
+
- **Mechanically enforced, not honor-system** — a generated `.claude/settings.json` Stop hook
|
|
125
|
+
refuses "done" unless a fresh, evidence-bound PASS receipt covers the changed surface (content-
|
|
126
|
+
hash bound, rebase/merge-proof), and CI independently re-checks the committed receipt still
|
|
127
|
+
attests `HEAD`. Proven against real violations: hardcoded colors, illegal layer imports, deleted
|
|
128
|
+
spec tests, and structural regressions are each refused and named by clause, 4/4 in the
|
|
129
|
+
repo's refusal-demo script.
|
|
108
130
|
- **A toolchain doctor** — diagnoses *and* heals JDK, Android SDK + AVD, Xcode/CLT, CocoaPods,
|
|
109
|
-
XcodeGen, Appium + drivers, Node. Idempotent, OS-aware, consent-gated.
|
|
131
|
+
XcodeGen, Appium + drivers (the legacy pre-Maestro path), Node. Idempotent, OS-aware, consent-gated.
|
|
132
|
+
The E2E flows themselves run on Maestro — install with
|
|
133
|
+
`curl -fsSL https://get.maestro.mobile.dev | bash`.
|
|
110
134
|
- **AI-inspectable by default** — every generated app ships a debug-only live inspector
|
|
111
135
|
(`127.0.0.1:9500`, loopback, structurally absent from release). An agent runs `connect_live` and
|
|
112
136
|
reads the running UI as JSON — hierarchy, geometry, resolved design tokens, real navigation
|
|
@@ -134,7 +158,7 @@ placeholders and clear "drop your Firebase config here" instructions.
|
|
|
134
158
|
| Auth | `email` / `phone` / `both` / `none` | `both` |
|
|
135
159
|
| Firebase services | Firestore · Storage · Functions · FCM | all on |
|
|
136
160
|
| Room local cache | on / off | on |
|
|
137
|
-
|
|
|
161
|
+
| E2E flows (Maestro) | on / off | on |
|
|
138
162
|
| Bottom-nav tabs | label + icon, any count | Home, Profile |
|
|
139
163
|
|
|
140
164
|
_(Web/PWA is intentionally out of scope — Android + iOS only.)_
|
|
@@ -149,6 +173,10 @@ shared engine, two front doors. Install from the bundled marketplace:
|
|
|
149
173
|
/plugin install create-cmp
|
|
150
174
|
```
|
|
151
175
|
|
|
176
|
+
No `/plugin` UI available (headless/non-interactive sessions)? See
|
|
177
|
+
[the settings.json fallback in `docs/USAGE.md`](./docs/USAGE.md#2-setup) — it's a real two-step
|
|
178
|
+
install (marketplace registration, then plugin install), not a one-line tweak.
|
|
179
|
+
|
|
152
180
|
It bundles eight skills (plus the `cmp-inspector` MCP server), each with a deterministic engine
|
|
153
181
|
behind it:
|
|
154
182
|
|
|
@@ -164,9 +192,10 @@ behind it:
|
|
|
164
192
|
- **cmp-firebase-connect** — post-scaffold onboarding: create/reuse a Firebase project, register
|
|
165
193
|
the app, and replace the placeholder config via the Firebase CLI (consent-gated), proven by a
|
|
166
194
|
green build.
|
|
167
|
-
- **cmp-test** — generate the
|
|
168
|
-
|
|
169
|
-
- **cmp-qa-prep** — brings up the emulator +
|
|
195
|
+
- **cmp-test** — generate the regression suite by *observing* the app: read the running UI's
|
|
196
|
+
semantics tree and emit Maestro E2E flows plus golden-tree snapshots in the shipped harness style.
|
|
197
|
+
- **cmp-qa-prep** — brings up the emulator + Maestro flow run + smoke (legacy Appium bring-up
|
|
198
|
+
path also supported).
|
|
170
199
|
|
|
171
200
|
## Why CMP, not React Native
|
|
172
201
|
|
|
@@ -179,8 +208,8 @@ app is **time-to-first-green-build**, and that's a tooling problem, not a merits
|
|
|
179
208
|
- **Native performance and platform access** without the RN bridge tax.
|
|
180
209
|
- **Reproducible by construction.** A frozen, version-locked, CI-gated template means the build that
|
|
181
210
|
was green yesterday is green today — the exact property ad-hoc CMP setups lack.
|
|
182
|
-
- **Proven, not assumed.** Every scaffold ends on
|
|
183
|
-
GREEN/FAIL verdict.
|
|
211
|
+
- **Proven, not assumed.** Every scaffold ends on the verify lane — real build + the full JVM
|
|
212
|
+
test tier — with a typed GREEN/FAIL verdict and an evidence receipt.
|
|
184
213
|
|
|
185
214
|
If `create-next-app` made React the default for the web by removing setup friction, the goal here is
|
|
186
215
|
the same for multiplatform mobile.
|
|
@@ -191,35 +220,44 @@ the same for multiplatform mobile.
|
|
|
191
220
|
Front doors: npx create-cmp-cli + Claude Code plugin (cmp-new / cmp-doctor / cmp-qa-prep)
|
|
192
221
|
│ one shared engine, two front doors
|
|
193
222
|
Engine (Node, deterministic): copy → token-replace → rename packages → toggle features → VERIFY
|
|
194
|
-
Golden template (frozen, CI'd): pinned versions · iOS shell · nav+insets · Clean Arch · DI ·
|
|
223
|
+
Golden template (frozen, CI'd): pinned versions · iOS shell · nav+insets · Clean Arch · DI · test pyramid
|
|
195
224
|
```
|
|
196
225
|
|
|
197
226
|
The engine never puts an LLM in the hot path: it copies the template, replaces placeholders in file
|
|
198
227
|
contents *and* paths, atomically renames package directories, toggles features (Firebase / auth type
|
|
199
|
-
/ Room /
|
|
200
|
-
|
|
228
|
+
/ Room / the e2e harness — feature key `e2e`, renamed from `appium` in 0.3.0; the old
|
|
229
|
+
`--no-appium` flag still works as a deprecated alias), and runs the verify
|
|
230
|
+
build. Determinism is the moat. See
|
|
231
|
+
[`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) for the full design, and
|
|
232
|
+
[`docs/DOCUMENTATION.md`](./docs/DOCUMENTATION.md) for the map of every doc — what's
|
|
233
|
+
authoritative for what, and the standards each implements.
|
|
201
234
|
|
|
202
235
|
## Requirements
|
|
203
236
|
|
|
204
237
|
- **Node.js ≥ 18** to run the scaffolder.
|
|
205
238
|
- **macOS** for iOS output (Xcode, CocoaPods, XcodeGen). Android output works on macOS or Linux.
|
|
206
|
-
- Everything else — JDK 17, Android SDK + emulator, CocoaPods, XcodeGen, Appium + drivers
|
|
207
|
-
built-in `doctor` detects and (with your consent) installs.
|
|
208
|
-
|
|
239
|
+
- Everything else — JDK 17, Android SDK + emulator, CocoaPods, XcodeGen, Appium + drivers (legacy
|
|
240
|
+
e2e path) — the built-in `doctor` detects and (with your consent) installs. The E2E flows
|
|
241
|
+
themselves run on Maestro, installed separately with
|
|
242
|
+
`curl -fsSL https://get.maestro.mobile.dev | bash`. Xcode itself must be installed from the App
|
|
243
|
+
Store; the doctor surfaces that as the one manual step.
|
|
209
244
|
|
|
210
245
|
## Roadmap
|
|
211
246
|
|
|
212
247
|
The full plan — six pillars from scaffold to store release, and the sequencing — lives in
|
|
213
|
-
[`docs/ROADMAP.md`](./docs/ROADMAP.md).
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
- [
|
|
248
|
+
[`docs/ROADMAP.md`](./docs/ROADMAP.md). The AI delivery harness itself (exemplars → conformance
|
|
249
|
+
gates → in-project generation → mechanical enforcement → observability) is **complete** — see
|
|
250
|
+
[`docs/HARNESS-PLAN.md`](./docs/HARNESS-PLAN.md) for the per-layer detail. Near-term:
|
|
251
|
+
|
|
252
|
+
- [x] Publish to npm as `create-cmp-cli` (`npx create-cmp-cli@latest`) — **live on the registry**;
|
|
253
|
+
`0.3.0` (the harness release) is prepared and pending its own publish.
|
|
254
|
+
- [x] AI-native Compose inspector — read a running app's hierarchy, geometry, and *resolved design
|
|
255
|
+
tokens* as structured JSON (no screenshots), including **live on-device inspection** over a
|
|
256
|
+
loopback debug server. See [`docs/INSPECTOR-PLAN.md`](./docs/INSPECTOR-PLAN.md).
|
|
257
|
+
- [x] Android build matrix in CI on every push; iOS matrix runs on manual dispatch.
|
|
258
|
+
- [ ] Publish `create-cmp-cli@0.3.0` to npm (needs an interactive OTP session) + record the
|
|
259
|
+
asciinema demo (scaffold → `add-feature` → gate refuses a violation → fix → PASS).
|
|
218
260
|
- [ ] More example features and nav shapes.
|
|
219
|
-
- **AI-native Compose inspector** — read a running app's hierarchy, geometry, and *resolved design
|
|
220
|
-
tokens* as structured JSON (no screenshots). **Phase 0 (headless host-JVM render → inspect) is
|
|
221
|
-
built and verified**; live-emulator inspection is next. See
|
|
222
|
-
[`docs/INSPECTOR-PLAN.md`](./docs/INSPECTOR-PLAN.md).
|
|
223
261
|
|
|
224
262
|
## Contributing
|
|
225
263
|
|
package/bin/create-cmp.mjs
CHANGED
|
@@ -74,8 +74,9 @@ function printHelp() {
|
|
|
74
74
|
`create (scaffold) flags:\n` +
|
|
75
75
|
` --name --package --bundle-id --region --theme-prefix\n` +
|
|
76
76
|
` --ios/--no-ios --firebase/--no-firebase --auth <email|phone|both|none>\n` +
|
|
77
|
-
` --room/--no-room --
|
|
77
|
+
` --room/--no-room --e2e/--no-e2e --inspector/--no-inspector\n` +
|
|
78
78
|
` --dev-client/--no-dev-client (desktop JVM window + Compose Hot Reload)\n` +
|
|
79
|
+
` (--appium/--no-appium accepted as deprecated aliases for --e2e/--no-e2e)\n` +
|
|
79
80
|
` --tabs Home:home,Profile:person\n` +
|
|
80
81
|
` --target-dir --verify/--no-verify --yes --force --dry-run-verify\n\n` +
|
|
81
82
|
`doctor flags: --yes --dry-run --no-ios --no-install --target-dir <dir> --fix\n` +
|
package/options.schema.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"platforms",
|
|
15
15
|
"firebase",
|
|
16
16
|
"room",
|
|
17
|
-
"
|
|
17
|
+
"e2e",
|
|
18
18
|
"inspector",
|
|
19
19
|
"devClient",
|
|
20
20
|
"tabs",
|
|
@@ -78,7 +78,10 @@
|
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"room": { "type": "boolean" },
|
|
81
|
-
"
|
|
81
|
+
"e2e": {
|
|
82
|
+
"type": "boolean",
|
|
83
|
+
"description": "Device-level E2E harness (Maestro flows in qa/e2e/). Feature key renamed from `appium` to `e2e` in 0.3.0."
|
|
84
|
+
},
|
|
82
85
|
"inspector": {
|
|
83
86
|
"type": "boolean",
|
|
84
87
|
"description": "Live on-device inspector: debug-only loopback HTTP server (127.0.0.1:9500) serving the Compose semantics tree + design-token catalog to the cmp-inspector MCP. Structurally absent from release builds."
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-cmp-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "The AI delivery harness for Kotlin/Compose Multiplatform — scaffolds a green-building app (Android + iOS) in minutes and holds AI-driven changes to a machine-enforced verify lane with a committed evidence receipt. Installs the `create-cmp` command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-cmp": "bin/create-cmp.mjs",
|
package/src/commands/create.mjs
CHANGED
|
@@ -4,6 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
import { flagBool } from "../lib/args.mjs";
|
|
6
6
|
|
|
7
|
+
// --- deprecated-flag compat -------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tri-state boolean flag with a deprecated alias: prefers `name`/`no-name`;
|
|
11
|
+
* if that's absent but the alias is present, uses it and prints a one-line
|
|
12
|
+
* deprecation warning. `--appium/--no-appium` was renamed to `--e2e/--no-e2e`
|
|
13
|
+
* in 0.3.0 (ADR-0002) — this keeps the old flag working.
|
|
14
|
+
*/
|
|
15
|
+
function flagBoolWithAlias(flags, name, aliasName, dflt) {
|
|
16
|
+
const primarySet = flags[name] !== undefined || flags[`no-${name}`] !== undefined;
|
|
17
|
+
if (primarySet) return flagBool(flags, name, dflt);
|
|
18
|
+
|
|
19
|
+
const aliasSet = flags[aliasName] !== undefined || flags[`no-${aliasName}`] !== undefined;
|
|
20
|
+
if (aliasSet) {
|
|
21
|
+
process.stderr.write(
|
|
22
|
+
`Warning: --${aliasName}/--no-${aliasName} is deprecated; use --${name}/--no-${name} instead.\n`
|
|
23
|
+
);
|
|
24
|
+
return flagBool(flags, aliasName, dflt);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return dflt;
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
// --- name helpers ------------------------------------------------------------
|
|
8
31
|
|
|
9
32
|
function parseTabs(str) {
|
|
@@ -61,7 +84,7 @@ function buildConfigFromFlags(flags, positional) {
|
|
|
61
84
|
fcm: flagBool(flags, "fcm", firebase),
|
|
62
85
|
},
|
|
63
86
|
room: flagBool(flags, "room", true),
|
|
64
|
-
|
|
87
|
+
e2e: flagBoolWithAlias(flags, "e2e", "appium", true),
|
|
65
88
|
inspector: flagBool(flags, "inspector", true),
|
|
66
89
|
devClient: flagBool(flags, "dev-client", true),
|
|
67
90
|
tabs: parseTabs(flags.tabs) || [
|
|
@@ -135,7 +158,7 @@ async function interactiveConfig(positional) {
|
|
|
135
158
|
const extras = await prompts(
|
|
136
159
|
[
|
|
137
160
|
{ type: "confirm", name: "room", message: "Room local cache?", initial: true },
|
|
138
|
-
{ type: "confirm", name: "
|
|
161
|
+
{ type: "confirm", name: "e2e", message: "E2E test harness (Maestro)?", initial: true },
|
|
139
162
|
{
|
|
140
163
|
type: "confirm",
|
|
141
164
|
name: "inspector",
|
|
@@ -173,7 +196,7 @@ async function interactiveConfig(positional) {
|
|
|
173
196
|
platforms: { android: true, ios: base.ios },
|
|
174
197
|
firebase: { enabled: base.firebase, auth, firestore, storage, functions, fcm },
|
|
175
198
|
room: extras.room,
|
|
176
|
-
|
|
199
|
+
e2e: extras.e2e,
|
|
177
200
|
inspector: extras.inspector,
|
|
178
201
|
devClient: extras.devClient,
|
|
179
202
|
tabs: parseTabs(extras.tabs) || [{ label: "Home", icon: "home" }],
|
package/src/lib/toggle.mjs
CHANGED
|
@@ -86,7 +86,7 @@ export function stripFeatureBlocks(content, disabledFeatures) {
|
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* Map an engine config object to the set of DISABLED feature names that the
|
|
89
|
-
* manifest understands: ios, firebase, room,
|
|
89
|
+
* manifest understands: ios, firebase, room, e2e, inspector, dev-client.
|
|
90
90
|
* @param {object} config
|
|
91
91
|
* @returns {Set<string>}
|
|
92
92
|
*/
|
|
@@ -95,7 +95,7 @@ export function disabledFeaturesFromConfig(config) {
|
|
|
95
95
|
if (!config.platforms?.ios) disabled.add("ios");
|
|
96
96
|
if (!config.firebase?.enabled) disabled.add("firebase");
|
|
97
97
|
if (!config.room) disabled.add("room");
|
|
98
|
-
if (!config.
|
|
98
|
+
if (!config.e2e) disabled.add("e2e");
|
|
99
99
|
if (!config.inspector) disabled.add("inspector");
|
|
100
100
|
if (!config.devClient) disabled.add("dev-client");
|
|
101
101
|
return disabled;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-feature
|
|
3
|
+
description: >-
|
|
4
|
+
Add a new conforming vertical-slice feature (Screen + ViewModel + UseCase + Repository + spec
|
|
5
|
+
+ tests + golden tree + nav route + DI wiring) to this Compose Multiplatform app, cloned
|
|
6
|
+
deterministically from the `home` exemplar. Use this when the user wants to "add a feature",
|
|
7
|
+
"add a screen with data", "scaffold a feature", "create a new screen backed by a repository",
|
|
8
|
+
"add a list screen", or names a new domain noun they want a screen for (e.g. "add a Favorites
|
|
9
|
+
feature", "I need a Bookmarks screen"). Works with NO create-cmp plugin installed — the
|
|
10
|
+
stamper (`qa/scaffold-feature.mjs`) and this skill both ship inside the generated project.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# add-feature — stamp a conforming vertical slice
|
|
14
|
+
|
|
15
|
+
> Spec-first, deterministic-stamp, gate-proven. The script (`qa/scaffold-feature.mjs`) does the
|
|
16
|
+
> mechanical work — copy the `home` exemplar file set, whole-word identifier rename, anchor
|
|
17
|
+
> injection into the three shared files. You (the AI) only refine spec wording and adapt the
|
|
18
|
+
> feature to its real shape. You are not done until `node qa/verify.mjs` PASSes and the receipt
|
|
19
|
+
> is committed — see this project's `CLAUDE.md`.
|
|
20
|
+
|
|
21
|
+
## Why a stamper and not hand-written files
|
|
22
|
+
|
|
23
|
+
This project's whole thesis is that determinism beats freehand generation for anything
|
|
24
|
+
architecturally load-bearing (see `docs/adr/0001-*.md` if present, or just: every hand-written
|
|
25
|
+
file is a drift chance). `qa/scaffold-feature.mjs` produces a **conforming skeleton by
|
|
26
|
+
construction** — it passes the architecture conformance gates before you write a line of
|
|
27
|
+
feature-specific logic. Your job is to make it *behave* like the real feature, not to make it
|
|
28
|
+
*structurally correct* — that part is already done.
|
|
29
|
+
|
|
30
|
+
## The flow
|
|
31
|
+
|
|
32
|
+
### 1. Interview
|
|
33
|
+
|
|
34
|
+
Ask the human for the feature name (PascalCase, plural-ish noun — e.g. `Favorites`,
|
|
35
|
+
`Bookmarks`). Propose a singular entity name by stripping a trailing `s`/`ies` (`Favorites` →
|
|
36
|
+
`Favorite`, `Categories` → `Category`). Naive de-pluralization is unreliable for irregular nouns
|
|
37
|
+
— **always show your proposed entity name and let the human confirm or override it** before
|
|
38
|
+
proceeding (`--entity <EntityName>`).
|
|
39
|
+
|
|
40
|
+
### 2. Dry-run
|
|
41
|
+
|
|
42
|
+
Run:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
node qa/scaffold-feature.mjs <FeatureName> --entity <EntityName> --dry-run
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Show the human the file plan and the anchor-injection diffs it prints. Confirm before stamping
|
|
49
|
+
for real — this is the last chance to catch a wrong entity name or a naming collision.
|
|
50
|
+
|
|
51
|
+
### 3. Stamp
|
|
52
|
+
|
|
53
|
+
Run the same command without `--dry-run`:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
node qa/scaffold-feature.mjs <FeatureName> --entity <EntityName>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This writes the new Screen/ViewModel/UseCase/Repository(+impl)/tests/fake, wires them into
|
|
60
|
+
`di/AppModule.kt`, `presentation/navigation/Screen.kt`, and `presentation/navigation/AppNavHost.kt`
|
|
61
|
+
at their `// cmp:anchor` markers, and writes `specs/<feature>.spec.md` with a default six-clause
|
|
62
|
+
set (`<FEATURE>-01..06`: loading, success, error, reload-after-failure, tap-navigates, golden
|
|
63
|
+
tree) — copied verbatim from the `home` exemplar's shape.
|
|
64
|
+
|
|
65
|
+
If it exits non-zero, read the message — it is actionable (name already taken, an anchor marker
|
|
66
|
+
is missing, or a name isn't a valid Kotlin identifier). Do not hand-edit around a stamper
|
|
67
|
+
failure; if an anchor is genuinely missing from a shared file, that is a template defect worth
|
|
68
|
+
flagging, not something to route around by hand-splicing.
|
|
69
|
+
|
|
70
|
+
### 4. Refine the spec, then the behavior
|
|
71
|
+
|
|
72
|
+
The default spec clauses are placeholders shaped like the `home` exemplar (a plain list of
|
|
73
|
+
title/subtitle rows). **Rewrite the clause prose** in `specs/<feature>.spec.md` to describe the
|
|
74
|
+
feature's real behavior — the six clause **ids stay fixed** (`specCoverage` binds tests to ids,
|
|
75
|
+
not prose), only the wording changes. Propose the rewritten clauses to the human; get them
|
|
76
|
+
confirmed before moving on — this project's contract is spec-first.
|
|
77
|
+
|
|
78
|
+
Then adapt the generated code to match:
|
|
79
|
+
|
|
80
|
+
- If the feature isn't shaped like "a list of `{id, title, subtitle}`", update the entity's
|
|
81
|
+
fields in `domain/model/<Entity>.kt`, the sample data in `<Entity>RepositoryImpl.kt`, and the
|
|
82
|
+
screen's rendering in `presentation/<feature>/<Feature>Screen.kt` together — keep them
|
|
83
|
+
consistent with each other and with the tests.
|
|
84
|
+
- Update the copied tests (`<Feature>ViewModelTest.kt`, `<Feature>ScreenTest.kt`) to match
|
|
85
|
+
whatever you changed. The gate (step 6) will tell you exactly what you missed — a compile
|
|
86
|
+
error names the mismatch; a spec-coverage failure names an orphaned clause or tag.
|
|
87
|
+
- Leave the DI wiring, nav route, and screen scaffold as stamped unless the feature genuinely
|
|
88
|
+
needs a different shape (e.g. no navigation-on-tap — then remove the `-05` clause's
|
|
89
|
+
citing test and strike the clause through, don't leave it dangling).
|
|
90
|
+
- The new screen is reachable via `Screen.<Feature>` but is **not** wired into a bottom-nav tab
|
|
91
|
+
by this generator (MVP scope — pushed-route only). Promoting it to a tab is a manual edit to
|
|
92
|
+
`appTabs()` + `AppShell` call sites; a future `--tab` flag may automate this.
|
|
93
|
+
|
|
94
|
+
### 5. Capture the golden tree
|
|
95
|
+
|
|
96
|
+
The golden baseline is **not** copied by the stamper (a copied one would silently mismatch the
|
|
97
|
+
adapted screen). Generate it fresh once the screen renders the real behavior:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
UPDATE_GOLDEN=1 ./gradlew :composeApp:desktopTest --tests "*<Feature>GoldenTree*"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Review `qa/golden/<feature>.json` briefly — it should reflect the structure you intended, not a
|
|
104
|
+
copy-paste artifact. Commit it alongside the feature.
|
|
105
|
+
|
|
106
|
+
### 6. Gate
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
node qa/verify.mjs
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This must PASS. It proves: the spec's six clauses are all bound to a citing test
|
|
113
|
+
(`specCoverage`), the build compiles, unit tests pass (ViewModel + UseCase + Repository +
|
|
114
|
+
fakes), architecture conformance holds (`presentation` doesn't import `data`, the new
|
|
115
|
+
`*Screen.kt` carries a `testTag`, the new `*ViewModel.kt` has a matching test), the golden tree
|
|
116
|
+
matches what you just captured, and accessibility holds. **Not done until this is PASS and the
|
|
117
|
+
evidence receipt (`qa/evidence/latest.json`) is committed with your change** — this is this
|
|
118
|
+
project's standing definition of done (see `CLAUDE.md`).
|
|
119
|
+
|
|
120
|
+
If it fails: read the failing step's reason (it is worded for exactly this), fix the actual
|
|
121
|
+
behavior or spec/test binding, and re-run. Do not delete or weaken a test to reach green.
|
|
122
|
+
|
|
123
|
+
## Guardrails
|
|
124
|
+
|
|
125
|
+
- This flow works identically with or without the create-cmp Claude Code plugin installed —
|
|
126
|
+
everything it needs (`qa/scaffold-feature.mjs`, this file) ships inside the generated project.
|
|
127
|
+
- Respect whichever toggles this project was stamped with (e.g. if Room or Appium/Maestro were
|
|
128
|
+
disabled at scaffold time, don't reintroduce them for the new feature).
|
|
129
|
+
- `add-feature` generates a pushed-nav-route screen, not a bottom-nav tab, and does not generate
|
|
130
|
+
a "tap → detail" destination (the exemplar's `DetailScreen.kt` is intentionally not copied —
|
|
131
|
+
MVP scope). Both are documented future extensions, not omissions to silently work around.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-repository
|
|
3
|
+
description: >-
|
|
4
|
+
Add a data/domain slice ONLY — model, repository interface + impl, use case, and a
|
|
5
|
+
hand-written fake — to this Compose Multiplatform app, cloned deterministically from the
|
|
6
|
+
`home` exemplar's data layer. No screen, no ViewModel, no nav route, no spec clauses. Use this
|
|
7
|
+
when the user wants to "add a repository", "add a data source", "model a new entity", "new
|
|
8
|
+
entity with a repository", or names a domain noun they want backed by data before any UI exists
|
|
9
|
+
(e.g. "add a Tag repository", "I need a data source for Bookmarks"). Works with NO create-cmp
|
|
10
|
+
plugin installed — the stamper (`qa/scaffold-feature.mjs --preset repository`) and this skill
|
|
11
|
+
both ship inside the generated project.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# add-repository — stamp a data/domain slice only
|
|
15
|
+
|
|
16
|
+
> Deterministic-stamp, gate-proven. The script (`qa/scaffold-feature.mjs --preset repository`)
|
|
17
|
+
> does the mechanical work — copy the `home` exemplar's data/domain files, whole-word identifier
|
|
18
|
+
> rename, DI injection for the repository + use case only. You (the AI) only customize the
|
|
19
|
+
> entity's fields and its sample data. You are not done until `node qa/verify.mjs` PASSes and the
|
|
20
|
+
> receipt is committed — see this project's `CLAUDE.md`.
|
|
21
|
+
|
|
22
|
+
This is the `repository` subset of `add-feature` — same stamper, same rename mechanic, filtered
|
|
23
|
+
to just the 5 data/domain files. Use it when you want the data layer to exist before any screen
|
|
24
|
+
consumes it (e.g. you're modeling several entities up front, or a screen for this entity will
|
|
25
|
+
come later via `add-screen`).
|
|
26
|
+
|
|
27
|
+
## Why a stamper and not hand-written files
|
|
28
|
+
|
|
29
|
+
Every hand-written file is a drift chance in this project's architecture. `qa/scaffold-feature.mjs
|
|
30
|
+
--preset repository` produces a **conforming skeleton by construction** for the data/domain
|
|
31
|
+
layer: a domain model, a domain repository interface, a use case, a data-layer impl, and a
|
|
32
|
+
hand-written fake for tests — wired into Koin. Your job is to make the entity's shape and sample
|
|
33
|
+
data real, not to make the wiring correct — that part is already done.
|
|
34
|
+
|
|
35
|
+
## The flow
|
|
36
|
+
|
|
37
|
+
### 1. Name the entity
|
|
38
|
+
|
|
39
|
+
Ask the human for the entity name — PascalCase, singular (e.g. `Tag`, `Bookmark`, `Category`).
|
|
40
|
+
Unlike `add-feature`/`add-screen`, there is no feature name and no `--entity` flag here: the
|
|
41
|
+
positional argument **is** the entity.
|
|
42
|
+
|
|
43
|
+
### 2. Dry-run
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
node qa/scaffold-feature.mjs <Entity> --preset repository --dry-run
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Show the human the file plan (5 files: `domain/model/<Entity>.kt`,
|
|
50
|
+
`domain/repository/<Entity>Repository.kt`, `domain/usecase/Get<Entity>sUseCase.kt`,
|
|
51
|
+
`data/remote/<Entity>RepositoryImpl.kt`, `testing/fakes/Fake<Entity>Repository.kt`) and the DI
|
|
52
|
+
injection diffs (repository binding + use case factory only — no ViewModel, no nav). Confirm
|
|
53
|
+
before stamping for real.
|
|
54
|
+
|
|
55
|
+
### 3. Stamp
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
node qa/scaffold-feature.mjs <Entity> --preset repository
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If it exits non-zero, read the message — it is actionable (entity name already taken, or not a
|
|
62
|
+
valid Kotlin identifier). Do not hand-edit around a stamper failure.
|
|
63
|
+
|
|
64
|
+
### 4. Customize the entity and its data
|
|
65
|
+
|
|
66
|
+
The generated shape mirrors `home`'s `Item` (an `{id, title, subtitle}` list entity). Adapt it to
|
|
67
|
+
the real entity:
|
|
68
|
+
|
|
69
|
+
- Update the fields in `domain/model/<Entity>.kt` to match the real shape.
|
|
70
|
+
- Update the sample/seed data in `<Entity>RepositoryImpl.kt` accordingly.
|
|
71
|
+
- Update `Fake<Entity>Repository.kt` (`testing/fakes/`) to match the model's new fields — it's
|
|
72
|
+
the hand-written fake every future test against this entity will use.
|
|
73
|
+
- Leave the DI wiring (`di/AppModule.kt`) as stamped unless you're renaming something — it's
|
|
74
|
+
already correct: `single<<Entity>Repository> { <Entity>RepositoryImpl() }` and
|
|
75
|
+
`factory { Get<Entity>sUseCase(get()) }`.
|
|
76
|
+
|
|
77
|
+
**No spec clauses are added by this preset, and that's intentional** — a bare repository has no
|
|
78
|
+
observable behavior to specify yet (no screen, no user-facing flow). Clauses attach once a
|
|
79
|
+
screen consumes this entity; that's `add-screen`'s job (`FEATURE-01..06` bound to the repository
|
|
80
|
+
you just built).
|
|
81
|
+
|
|
82
|
+
### 5. Gate
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
node qa/verify.mjs
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This must PASS. It proves: the build compiles, unit tests pass (use case + repository + fake),
|
|
89
|
+
and architecture conformance holds (`domain` imports nothing app-internal, `data` implements the
|
|
90
|
+
`domain` interface). specCoverage is untouched by this preset — expect the same clause/tag counts
|
|
91
|
+
as before you ran the stamper. **Not done until this is PASS and the evidence receipt
|
|
92
|
+
(`qa/evidence/latest.json`) is committed with your change.**
|
|
93
|
+
|
|
94
|
+
If it fails: read the failing step's reason and fix the actual behavior. Do not delete or weaken
|
|
95
|
+
a test to reach green.
|
|
96
|
+
|
|
97
|
+
## Guardrails
|
|
98
|
+
|
|
99
|
+
- Works identically with or without the create-cmp Claude Code plugin installed — everything it
|
|
100
|
+
needs (`qa/scaffold-feature.mjs`, this file) ships inside the generated project.
|
|
101
|
+
- Respect whichever toggles this project was stamped with (e.g. Room disabled at scaffold time
|
|
102
|
+
stays disabled — this preset never introduces Room coupling, matching the exemplar's
|
|
103
|
+
dependency-light `RepositoryImpl` pattern).
|
|
104
|
+
- This preset stamps data/domain only. If the human actually wants a screen too, either run
|
|
105
|
+
`add-feature` (the full slice) instead, or follow up with `add-screen --entity <Entity>` once
|
|
106
|
+
this repository exists.
|