cmp-upgrade — move a KMP project to a proven-green version set
Your job: turn "upgrade my KMP versions" (or "my build broke after a version bump") into a project
sitting on a proven-green version set — one that is known to build on Android + iOS — without
hand-editing versions one at a time. You wrap the engine's upgrade command (exposed on the CLI as
the upgrade subcommand). Do not hand-edit libs.versions.toml yourself when the engine can do
it — the engine diffs, guards the lockstep, writes surgically, and backs up.
Why this exists. The recurring pain of KMP is that Kotlin / KSP / Compose / Room / AGP move in lockstep or the build dies. Upgrading "just Kotlin" is how projects break:
kspmust always be<kotlin>-<kspVersion>(e.g. kotlin2.2.20↔ ksp2.2.20-2.0.4), Room on iOS needsksp.useKSP2=true, and AGP pins a minimum Gradle wrapper. A proven-green set moves all of them together — one command instead of a day of dependency archaeology.
The workflow: diff → apply → verify
# 1. DIFF (safe, writes nothing) — show what the upgrade would change:
node <repo>/bin/create-cmp.mjs upgrade --dry-run
# (add --target-dir <dir> when not running from the project root,
# --set <id> to target a specific registry set instead of the latest)
# 2. APPLY — after the user has seen and accepted the diff:
node <repo>/bin/create-cmp.mjs upgrade --yes
# 3. VERIFY — prove the build is green (or chain it: upgrade --yes --verify):
node <repo>/bin/create-cmp.mjs verify
(If invoked from the published package: npx create-cmp-cli@latest upgrade ….)
What the engine does when applying:
- Rewrites only the changed version values in
gradle/libs.versions.toml— surgical line edits; comments, formatting, and every unrelated line are preserved byte-for-byte. - Adds/updates the
gradle.propertiesflags the set requires (e.g.ksp.useKSP2=true). - Updates the Gradle wrapper
distributionUrlwhen the set pins one. - Rewrites
compileSdk/targetSdkincomposeApp/build.gradle.ktswhen the set pins them (itsandroidSdkblock) — the Android SDK levels are coupled to the set (a newer AGP + newer androidx force a highercompileSdk), so the version set manages them too, not just the catalog. - Backs up every touched file as
<file>.bak-upgradebefore writing and prints the exactmvcommands to revert. - Versions the project declares that the set doesn't know are left untouched (and warned), so project-specific dependencies survive.
The lockstep guardrail — relay it, don't fight it
The engine refuses to write any file where the resulting ksp is not in lockstep with
kotlin — either <kotlin> (the KSP2 aligned scheme, from Kotlin 2.3) or <kotlin>-<kspVersion>
(the classic KSP1 form). If it errors with a lockstep violation, that is the product working as
designed: the target set or the
project's partial state would produce a known-dead build. Fix by upgrading kotlin and ksp
together (which is exactly what a registry set does) — never by overriding one of them by hand.
Consent gating
Applying mutates the user's build files. Default behavior is a dry run: without --yes (or an
interactive "y"), nothing is written. Show the user the diff table first and get their go-ahead
before passing --yes. Never skip straight to apply on a project you haven't diffed.
Any project welcome
This works on any Gradle project with a gradle/libs.versions.toml, whoever scaffolded it.
The engine detects the create-cmp frozen-set marker only to adjust messaging — for non-create-cmp
projects, review the diff with extra care (their catalog may pin versions for reasons the registry
doesn't know), and lean on the warned "left untouched" list.
Report
Tell the user: which set was targeted (and its notes — they explain the lockstep and the KSP2/iOS
catch-22), the change table, which files were written and where the .bak-upgrade backups are, the
revert commands, and the GREEN/FAIL verdict if --verify/verify was run. If the build is not
green afterwards, revert with the printed mv commands and run cmp-doctor to diagnose. If the
toolchain itself is incomplete, point them at cmp-doctor first.