create-cmp-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/bin/create-cmp.mjs +91 -0
  4. package/options.schema.json +110 -0
  5. package/package.json +52 -0
  6. package/src/bootstrap/checks.mjs +283 -0
  7. package/src/bootstrap/exec.mjs +90 -0
  8. package/src/commands/clean.mjs +195 -0
  9. package/src/commands/create.mjs +223 -0
  10. package/src/commands/doctor.mjs +256 -0
  11. package/src/commands/upgrade.mjs +196 -0
  12. package/src/commands/verify.mjs +94 -0
  13. package/src/doctor.mjs +109 -0
  14. package/src/lib/args.mjs +38 -0
  15. package/src/lib/clean.mjs +72 -0
  16. package/src/lib/fsutil.mjs +90 -0
  17. package/src/lib/log.mjs +40 -0
  18. package/src/lib/project-doctor.mjs +329 -0
  19. package/src/lib/registry.mjs +102 -0
  20. package/src/lib/rename.mjs +116 -0
  21. package/src/lib/schema.mjs +123 -0
  22. package/src/lib/toggle.mjs +130 -0
  23. package/src/lib/tokens.mjs +96 -0
  24. package/src/lib/toml.mjs +137 -0
  25. package/src/lib/upgrade.mjs +159 -0
  26. package/src/lib/verify.mjs +97 -0
  27. package/src/scaffold.mjs +347 -0
  28. package/src/verify.mjs +4 -0
  29. package/src/versions/registry.json +42 -0
  30. package/template/.github/workflows/verify.yml +70 -0
  31. package/template/build.gradle.kts +17 -0
  32. package/template/composeApp/build.gradle.kts +257 -0
  33. package/template/composeApp/google-services.json +29 -0
  34. package/template/composeApp/proguard-rules.pro +34 -0
  35. package/template/composeApp/src/androidDebug/AndroidManifest.xml +13 -0
  36. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/ComposeRootRegistry.kt +40 -0
  37. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorCatalog.kt +80 -0
  38. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt +322 -0
  39. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +16 -0
  40. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/LiveSemanticsJson.kt +102 -0
  41. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/RemoteControlPage.kt +106 -0
  42. package/template/composeApp/src/androidDebug/res/xml/debug_network_security_config.xml +9 -0
  43. package/template/composeApp/src/androidMain/AndroidManifest.xml +28 -0
  44. package/template/composeApp/src/androidMain/kotlin/com/example/app/AppApplication.kt +72 -0
  45. package/template/composeApp/src/androidMain/kotlin/com/example/app/MainActivity.kt +24 -0
  46. package/template/composeApp/src/androidMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +42 -0
  47. package/template/composeApp/src/androidMain/kotlin/com/example/app/data/local/DatabaseBuilder.android.kt +13 -0
  48. package/template/composeApp/src/androidMain/kotlin/com/example/app/di/AndroidModule.kt +9 -0
  49. package/template/composeApp/src/androidMain/kotlin/com/example/app/presentation/components/TestTagAutomation.android.kt +11 -0
  50. package/template/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml +9 -0
  51. package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
  52. package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  53. package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png +0 -0
  54. package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  55. package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  56. package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png +0 -0
  57. package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  58. package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  59. package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png +0 -0
  60. package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  61. package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  62. package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  63. package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  64. package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  65. package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  66. package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  67. package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  68. package/template/composeApp/src/androidMain/res/values/colors.xml +6 -0
  69. package/template/composeApp/src/androidMain/res/values/themes.xml +9 -0
  70. package/template/composeApp/src/androidMain/res/values-v31/themes.xml +8 -0
  71. package/template/composeApp/src/androidRelease/kotlin/com/example/app/inspector/InspectorInit.kt +13 -0
  72. package/template/composeApp/src/commonMain/composeResources/font/DMSans_Bold.ttf +0 -0
  73. package/template/composeApp/src/commonMain/composeResources/font/DMSans_Medium.ttf +0 -0
  74. package/template/composeApp/src/commonMain/composeResources/font/DMSans_Regular.ttf +0 -0
  75. package/template/composeApp/src/commonMain/composeResources/font/DMSans_SemiBold.ttf +0 -0
  76. package/template/composeApp/src/commonMain/kotlin/com/example/app/App.kt +7 -0
  77. package/template/composeApp/src/commonMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +7 -0
  78. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/AppDatabase.kt +28 -0
  79. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/DatabaseBuilder.kt +14 -0
  80. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/ItemDao.kt +18 -0
  81. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/FirebaseConfig.kt +5 -0
  82. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt +22 -0
  83. package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +23 -0
  84. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/model/Item.kt +8 -0
  85. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/repository/ItemRepository.kt +8 -0
  86. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt +12 -0
  87. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/App.kt +13 -0
  88. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/BaseScreen.kt +69 -0
  89. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/TestTagAutomation.kt +18 -0
  90. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +39 -0
  91. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +87 -0
  92. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +35 -0
  93. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +43 -0
  94. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +144 -0
  95. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppTab.kt +25 -0
  96. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt +15 -0
  97. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +43 -0
  98. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/DesignToken.kt +25 -0
  99. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Shape.kt +12 -0
  100. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Theme.kt +64 -0
  101. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Tokens.kt +19 -0
  102. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Typography.kt +36 -0
  103. package/template/composeApp/src/desktopMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.desktop.kt +9 -0
  104. package/template/composeApp/src/desktopMain/kotlin/com/example/app/data/local/DatabaseBuilder.desktop.kt +13 -0
  105. package/template/composeApp/src/desktopMain/kotlin/com/example/app/di/DesktopModule.kt +32 -0
  106. package/template/composeApp/src/desktopMain/kotlin/com/example/app/main.kt +29 -0
  107. package/template/composeApp/src/desktopMain/kotlin/com/example/app/presentation/components/TestTagAutomation.desktop.kt +6 -0
  108. package/template/composeApp/src/iosMain/kotlin/com/example/app/KoinHelper.kt +50 -0
  109. package/template/composeApp/src/iosMain/kotlin/com/example/app/MainViewController.kt +10 -0
  110. package/template/composeApp/src/iosMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +32 -0
  111. package/template/composeApp/src/iosMain/kotlin/com/example/app/data/local/DatabaseBuilder.ios.kt +10 -0
  112. package/template/composeApp/src/iosMain/kotlin/com/example/app/presentation/components/TestTagAutomation.ios.kt +11 -0
  113. package/template/docs/dev-client.md +52 -0
  114. package/template/gitignore +21 -0
  115. package/template/gradle/libs.versions.toml +100 -0
  116. package/template/gradle/wrapper/gradle-wrapper.jar +0 -0
  117. package/template/gradle/wrapper/gradle-wrapper.properties +7 -0
  118. package/template/gradle.properties +9 -0
  119. package/template/gradlew +152 -0
  120. package/template/gradlew.bat +94 -0
  121. package/template/iosApp/Podfile +27 -0
  122. package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png +0 -0
  123. package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  124. package/template/iosApp/iosApp/Assets.xcassets/Contents.json +6 -0
  125. package/template/iosApp/iosApp/ContentView.swift +17 -0
  126. package/template/iosApp/iosApp/GoogleService-Info.plist +34 -0
  127. package/template/iosApp/iosApp/Info.plist +43 -0
  128. package/template/iosApp/iosApp/PrivacyInfo.xcprivacy +42 -0
  129. package/template/iosApp/iosApp/iOSApp.swift +33 -0
  130. package/template/iosApp/project.yml +56 -0
  131. package/template/local.properties.example +2 -0
  132. package/template/manifest.json +125 -0
  133. package/template/qa/appium/README.md +23 -0
  134. package/template/qa/appium/lib/appium-client.mjs +225 -0
  135. package/template/qa/appium/package.json +8 -0
  136. package/template/qa/appium/run-android-smoke.mjs +39 -0
  137. package/template/settings.gradle.kts +34 -0
  138. package/template/tests/appium/cmp/conftest.py +96 -0
  139. package/template/tests/appium/cmp/test_smoke.py +17 -0
@@ -0,0 +1,347 @@
1
+ // Scaffold pipeline (CONTRACT §"Scaffold pipeline"):
2
+ // (a) validate config against options.schema.json
3
+ // (b) copy template/ → targetDir
4
+ // (b.2) delete disabled-feature paths (MUST precede the package rename —
5
+ // feature paths are declared against the literal com/example/app)
6
+ // (c) token-replace contents AND paths
7
+ // (d) rename package source dirs com/example/app → __PACKAGE_PATH__ (atomic)
8
+ // (e) toggle features (strip cmp:feature marker blocks)
9
+ // (f) run the verify gate
10
+ //
11
+ // No LLM in this hot path — pure deterministic Node.
12
+
13
+ import fs from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ import { validate, formatErrors } from "./lib/schema.mjs";
19
+ import { buildTokenMap, replaceTokens, replacePathTokens, isBinaryPath, slugifyAppName } from "./lib/tokens.mjs";
20
+ import { renamePackageDirs } from "./lib/rename.mjs";
21
+ import {
22
+ stripFeatureBlocks,
23
+ disabledFeaturesFromConfig,
24
+ deleteDisabledFeaturePaths,
25
+ } from "./lib/toggle.mjs";
26
+ import { copyDir, listFiles, listDirsDeepestFirst } from "./lib/fsutil.mjs";
27
+ import { runVerify, printVerifyVerdict } from "./lib/verify.mjs";
28
+ import { colors, step, ok, warn } from "./lib/log.mjs";
29
+
30
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
31
+ const REPO_ROOT = path.resolve(__dirname, "..");
32
+
33
+ /**
34
+ * @param {object} opts
35
+ * @param {string} [opts.schemaPath] override path to options.schema.json
36
+ * @param {string} [opts.templateDir] override path to template/
37
+ */
38
+ export function loadSchema(opts = {}) {
39
+ const schemaPath = opts.schemaPath || path.join(REPO_ROOT, "options.schema.json");
40
+ return JSON.parse(fs.readFileSync(schemaPath, "utf8"));
41
+ }
42
+
43
+ /**
44
+ * Validate the engine config. Throws a descriptive error on failure.
45
+ * @param {object} config
46
+ * @param {object} [opts]
47
+ */
48
+ export function validateConfig(config, opts = {}) {
49
+ const schema = loadSchema(opts);
50
+ const { valid, errors } = validate(config, schema);
51
+ if (!valid) {
52
+ const err = new Error(`Invalid config:\n${formatErrors(errors)}`);
53
+ err.validationErrors = errors;
54
+ throw err;
55
+ }
56
+ return true;
57
+ }
58
+
59
+ /**
60
+ * Load template/manifest.json.
61
+ * @param {string} templateDir
62
+ */
63
+ export function loadManifest(templateDir) {
64
+ const p = path.join(templateDir, "manifest.json");
65
+ if (!fs.existsSync(p)) {
66
+ throw new Error(`template manifest not found at ${p}`);
67
+ }
68
+ return JSON.parse(fs.readFileSync(p, "utf8"));
69
+ }
70
+
71
+ /**
72
+ * Apply token replacement to every text file's CONTENT under projectDir.
73
+ */
74
+ function replaceContents(projectDir, tokenMap, manifestRel) {
75
+ for (const file of listFiles(projectDir)) {
76
+ // Never rewrite the manifest we copied in (and we delete it at the end).
77
+ if (manifestRel && path.resolve(file) === path.resolve(path.join(projectDir, manifestRel))) {
78
+ continue;
79
+ }
80
+ if (isBinaryPath(file)) continue;
81
+ let content;
82
+ try {
83
+ content = fs.readFileSync(file, "utf8");
84
+ } catch {
85
+ continue;
86
+ }
87
+ const replaced = replaceTokens(content, tokenMap);
88
+ if (replaced !== content) {
89
+ fs.writeFileSync(file, replaced);
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Rename directories whose names contain tokens (PATH replacement), deepest
96
+ * first so nested renames don't invalidate parent paths. Note: the package
97
+ * source dirs are renamed separately/atomically by renamePackageDirs against
98
+ * the literal com/example/app segment — here we only handle token-bearing
99
+ * path segments like __APP_NAME__ in a folder name.
100
+ */
101
+ function replaceDirPaths(projectDir, tokenMap) {
102
+ for (const dir of listDirsDeepestFirst(projectDir)) {
103
+ const base = path.basename(dir);
104
+ const renamedBase = replacePathTokens(base, tokenMap);
105
+ if (renamedBase !== base) {
106
+ const target = path.join(path.dirname(dir), renamedBase);
107
+ fs.mkdirSync(path.dirname(target), { recursive: true });
108
+ if (fs.existsSync(target)) {
109
+ // Merge into existing target.
110
+ for (const e of fs.readdirSync(dir)) {
111
+ fs.renameSync(path.join(dir, e), path.join(target, e));
112
+ }
113
+ fs.rmdirSync(dir);
114
+ } else {
115
+ fs.renameSync(dir, target);
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Rename token-bearing FILE names (path replacement for files).
123
+ */
124
+ function replaceFilePaths(projectDir, tokenMap) {
125
+ for (const file of listFiles(projectDir)) {
126
+ const base = path.basename(file);
127
+ const renamedBase = replacePathTokens(base, tokenMap);
128
+ if (renamedBase !== base) {
129
+ const target = path.join(path.dirname(file), renamedBase);
130
+ fs.renameSync(file, target);
131
+ }
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Strip cmp:feature marker blocks for disabled features from every text file.
137
+ * Always runs (even with an empty disabled set) so that ENABLED features' marker
138
+ * comment lines are also removed — the shipped output must carry no marker noise.
139
+ */
140
+ function stripDisabledBlocks(projectDir, disabled) {
141
+ for (const file of listFiles(projectDir)) {
142
+ if (isBinaryPath(file)) continue;
143
+ let content;
144
+ try {
145
+ content = fs.readFileSync(file, "utf8");
146
+ } catch {
147
+ continue;
148
+ }
149
+ if (!content.includes("cmp:feature")) continue;
150
+ const { content: out, changed } = stripFeatureBlocks(content, disabled);
151
+ if (changed) fs.writeFileSync(file, out);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Resolve the Android SDK location from the environment or the conventional
157
+ * install path, then write `local.properties` (sdk.dir) into the project so
158
+ * Gradle can locate the SDK. No-op if the file already exists (respect a
159
+ * user-provided one) or if no SDK dir can be found and env vars are unset.
160
+ * @param {string} projectDir
161
+ */
162
+ function writeLocalProperties(projectDir) {
163
+ const target = path.join(projectDir, "local.properties");
164
+ if (fs.existsSync(target)) return;
165
+
166
+ const envHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
167
+ const conventional = path.join(os.homedir(), "Library", "Android", "sdk");
168
+ let sdkDir = null;
169
+ if (envHome && fs.existsSync(envHome)) {
170
+ sdkDir = envHome;
171
+ } else if (fs.existsSync(conventional)) {
172
+ sdkDir = conventional;
173
+ } else if (fs.existsSync(path.join(os.homedir(), "Android", "Sdk"))) {
174
+ sdkDir = path.join(os.homedir(), "Android", "Sdk"); // Linux default
175
+ }
176
+ // If env vars are set, Gradle resolves the SDK on its own; only write the
177
+ // file when we actually located a directory.
178
+ if (!sdkDir) return;
179
+ fs.writeFileSync(target, `sdk.dir=${sdkDir}\n`);
180
+ }
181
+
182
+ /**
183
+ * Replace the (already token-substituted) app display name with a slugified,
184
+ * identifier-safe form in the few files that demand it: Gradle's
185
+ * rootProject.name and the appium npm package name. Idempotent and a no-op when
186
+ * the name is already slug-safe.
187
+ * @param {string} projectDir
188
+ * @param {string} appName raw display name (may contain spaces)
189
+ */
190
+ function applyAppNameSlug(projectDir, appName) {
191
+ const slug = slugifyAppName(appName);
192
+ if (slug === appName) return; // already safe — nothing to do
193
+
194
+ // settings.gradle.kts: rootProject.name = "Demo App" -> "Demo-App"
195
+ const settings = path.join(projectDir, "settings.gradle.kts");
196
+ if (fs.existsSync(settings)) {
197
+ const src = fs.readFileSync(settings, "utf8");
198
+ const out = src.replace(
199
+ /(rootProject\.name\s*=\s*")([^"]*)(")/,
200
+ (_m, a, _name, c) => `${a}${slug}${c}`
201
+ );
202
+ if (out !== src) fs.writeFileSync(settings, out);
203
+ }
204
+
205
+ // qa/appium/package.json: "name": "Demo App-appium" -> "demo-app-appium"
206
+ const pkg = path.join(projectDir, "qa", "appium", "package.json");
207
+ if (fs.existsSync(pkg)) {
208
+ try {
209
+ const json = JSON.parse(fs.readFileSync(pkg, "utf8"));
210
+ if (typeof json.name === "string" && json.name.includes(appName)) {
211
+ json.name = json.name.split(appName).join(slug).toLowerCase();
212
+ fs.writeFileSync(pkg, `${JSON.stringify(json, null, 2)}\n`);
213
+ }
214
+ } catch {
215
+ // leave as-is on parse failure
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Run the full scaffold pipeline.
222
+ * @param {object} config engine config object (CONTRACT)
223
+ * @param {object} [opts]
224
+ * @param {string} [opts.templateDir]
225
+ * @param {string} [opts.schemaPath]
226
+ * @param {boolean} [opts.verify=true]
227
+ * @param {boolean} [opts.dryRunVerify=false]
228
+ * @param {boolean} [opts.force=false] allow non-empty targetDir
229
+ * @returns {Promise<{projectDir:string, verdict:(object|null), manifest:object}>}
230
+ */
231
+ export async function scaffold(config, opts = {}) {
232
+ const templateDir = opts.templateDir || path.join(REPO_ROOT, "template");
233
+ const doVerify = opts.verify !== false;
234
+
235
+ // (a) validate
236
+ step("Validating config…");
237
+ validateConfig(config, { schemaPath: opts.schemaPath });
238
+
239
+ if (!fs.existsSync(templateDir)) {
240
+ throw new Error(`template directory not found at ${templateDir}`);
241
+ }
242
+ const manifest = loadManifest(templateDir);
243
+
244
+ const projectDir = path.resolve(config.targetDir);
245
+ if (fs.existsSync(projectDir)) {
246
+ const entries = fs.readdirSync(projectDir).filter((e) => e !== "." && e !== "..");
247
+ if (entries.length > 0 && !opts.force) {
248
+ throw new Error(
249
+ `target directory ${projectDir} is not empty (pass force to overwrite)`
250
+ );
251
+ }
252
+ }
253
+
254
+ // (b) copy template → targetDir
255
+ step(`Copying template → ${colors.cyan(projectDir)}`);
256
+ copyDir(templateDir, projectDir);
257
+
258
+ // (b.1) `npm pack` ALWAYS strips files named `.gitignore` from published
259
+ // tarballs, so the template ships it as `gitignore` (no dot) and the stamp
260
+ // restores the real name here (the standard scaffolder workaround).
261
+ const bareGitignore = path.join(projectDir, "gitignore");
262
+ if (fs.existsSync(bareGitignore)) {
263
+ fs.renameSync(bareGitignore, path.join(projectDir, ".gitignore"));
264
+ }
265
+
266
+ // (b.2) Delete disabled-feature PATHS now, BEFORE the package-dir rename.
267
+ // Manifest feature paths are declared with the literal `com/example/app`
268
+ // segment; deleting after renamePackageDirs silently missed every
269
+ // package-rooted path (the --no-room bug: Room deps/plugin stripped but the
270
+ // data/local sources left behind → unresolved androidx.room at build time).
271
+ const disabled = disabledFeaturesFromConfig(config);
272
+ if (disabled.size > 0) {
273
+ step(`Deleting disabled-feature paths: ${[...disabled].join(", ")}`);
274
+ deleteDisabledFeaturePaths(projectDir, manifest, disabled, (m) =>
275
+ process.stdout.write(`${m}\n`)
276
+ );
277
+ }
278
+
279
+ const tokenMap = buildTokenMap(config);
280
+
281
+ // (c) token-replace contents AND paths
282
+ step("Replacing tokens (file contents)…");
283
+ replaceContents(projectDir, tokenMap, "manifest.json");
284
+
285
+ // (d) rename package source dirs (ATOMIC) — do this BEFORE generic path
286
+ // token replace so the literal com/example/app is intact.
287
+ step("Renaming package source directories…");
288
+ const packageRoots = manifest.packageSourceRoots || [];
289
+ const packagePath = config.package.replace(/\./g, "/");
290
+ renamePackageDirs(projectDir, packageRoots, packagePath, {
291
+ log: (m) => process.stdout.write(`${m}\n`),
292
+ });
293
+
294
+ // (c, paths) token-replace remaining token-bearing dir + file names
295
+ step("Replacing tokens (paths)…");
296
+ replaceDirPaths(projectDir, tokenMap);
297
+ replaceFilePaths(projectDir, tokenMap);
298
+
299
+ // (c.1) Slugify the app name where a space-free identifier is required.
300
+ // The display name (__APP_NAME__) may contain spaces (Android label, iOS
301
+ // CFBundleDisplayName), but Gradle rootProject.name and the npm package name
302
+ // must not. See manifest stampPipeline step 5.
303
+ applyAppNameSlug(projectDir, config.appName);
304
+
305
+ // (e) toggle feature MARKER BLOCKS. Always strip marker comments (even for
306
+ // enabled features) so no marker noise ships. (Disabled-feature paths were
307
+ // already deleted in (b.2), before the package rename — see above.)
308
+ step(
309
+ disabled.size > 0
310
+ ? `Toggling features off: ${[...disabled].join(", ")}`
311
+ : "Cleaning feature markers…"
312
+ );
313
+ stripDisabledBlocks(projectDir, disabled);
314
+
315
+ // Write local.properties (sdk.dir) so the Gradle build can find the Android
316
+ // SDK even when ANDROID_HOME/ANDROID_SDK_ROOT aren't exported (manifest
317
+ // stampPipeline step 7). Skip silently if no SDK is found and env vars are
318
+ // unset — the verify step will surface the real error.
319
+ writeLocalProperties(projectDir);
320
+
321
+ // Drop engine-only artifacts from the stamped output (the manifest and any
322
+ // template extraction notes — engine-only handoff files that are not part of
323
+ // a generated app).
324
+ for (const artifact of ["manifest.json", "EXTRACTION-NOTES.md"]) {
325
+ const abs = path.join(projectDir, artifact);
326
+ if (fs.existsSync(abs)) fs.rmSync(abs);
327
+ }
328
+
329
+ ok("Scaffold complete.");
330
+
331
+ // (f) verify gate
332
+ let verdict = null;
333
+ if (doVerify) {
334
+ step("Running verify gate (north-star: prove GREEN)…");
335
+ verdict = await runVerify({
336
+ projectDir,
337
+ manifest,
338
+ config,
339
+ dryRun: !!opts.dryRunVerify,
340
+ });
341
+ printVerifyVerdict(verdict);
342
+ } else {
343
+ warn("Verify skipped (--no-verify): GREEN build is NOT proven.");
344
+ }
345
+
346
+ return { projectDir, verdict, manifest };
347
+ }
package/src/verify.mjs ADDED
@@ -0,0 +1,4 @@
1
+ // Public entry for the verify gate. The implementation lives in lib/verify.mjs;
2
+ // this re-export keeps `src/verify.mjs` as the documented north-star module
3
+ // (CONTRACT §"North-star gate").
4
+ export { runVerify, printVerifyVerdict } from "./lib/verify.mjs";
@@ -0,0 +1,42 @@
1
+ {
2
+ "description": "Ordered registry of PROVEN-GREEN CMP version sets. Entries are ordered oldest → newest; the LAST entry is the default `create-cmp upgrade` target. A set is only added after it builds green on Android + iOS in CI (the canary feeds this file). Keys under `versions` mirror the [versions] table of the golden template's gradle/libs.versions.toml.",
3
+ "sets": [
4
+ {
5
+ "id": "2026.06",
6
+ "label": "Frozen, CI-verified set shipped with create-cmp 0.1.x",
7
+ "status": "proven-green",
8
+ "versions": {
9
+ "kotlin": "2.2.20",
10
+ "agp": "8.7.3",
11
+ "google-services": "4.4.2",
12
+ "compose-multiplatform": "1.10.3",
13
+ "koin": "4.1.1",
14
+ "ktor": "3.1.0",
15
+ "room": "2.8.4",
16
+ "sqlite": "2.6.2",
17
+ "ksp": "2.2.20-2.0.4",
18
+ "kotlinx-serialization": "1.7.3",
19
+ "kotlinx-datetime": "0.7.0",
20
+ "lifecycle": "2.10.0",
21
+ "navigation": "2.9.2",
22
+ "firebase-gitlive": "2.1.0",
23
+ "coil": "3.1.0",
24
+ "androidx-core": "1.15.0",
25
+ "androidx-activity": "1.10.1"
26
+ },
27
+ "gradleProperties": {
28
+ "ksp.useKSP2": "true"
29
+ },
30
+ "gradleWrapper": {
31
+ "version": "8.11.1",
32
+ "distributionUrl": "https://services.gradle.org/distributions/gradle-8.11.1-bin.zip"
33
+ },
34
+ "notes": [
35
+ "KSP must be `<kotlin>-<kspVersion>`: kotlin 2.2.20 pairs with ksp 2.2.20-2.0.4. Never bump one without the other — Kotlin / KSP / Compose / Room / AGP move as ONE set or the build dies.",
36
+ "ksp.useKSP2=true is REQUIRED when Room's KSP processor runs on Kotlin/Native (iOS): KSP1 fails with `ClassNotFoundException: org.jetbrains.kotlin.cli.utilities.MainKt` (the KSP2/iOS catch-22).",
37
+ "Room 2.8.4 + sqlite-bundled 2.6.2 is the KMP-native pairing; Room < 2.7 has no Kotlin/Native support at all.",
38
+ "Gradle wrapper 8.11.1 matches AGP 8.7.3 (AGP 8.7 requires Gradle >= 8.9)."
39
+ ]
40
+ }
41
+ ]
42
+ }
@@ -0,0 +1,70 @@
1
+ # CI for your Compose Multiplatform app — stamped in by create-cmp.
2
+ #
3
+ # What runs on every push/PR: a toolchain report (advisory) and the Android
4
+ # debug build — the same "verify gate" create-cmp ran when it generated this
5
+ # project. Green here = your frozen version set still builds.
6
+ #
7
+ # iOS: a ready-to-enable macOS job is included (commented out) at the bottom.
8
+ # macOS runners cost ~10x Linux minutes, so it's opt-in.
9
+
10
+ name: Verify
11
+
12
+ on:
13
+ push:
14
+ branches: [main]
15
+ pull_request:
16
+
17
+ jobs:
18
+ android:
19
+ runs-on: ubuntu-latest # ships with JDK + Android SDK preinstalled
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: actions/setup-java@v4
24
+ with:
25
+ distribution: temurin
26
+ java-version: 17 # the version set is pinned against JDK 17
27
+
28
+ # Caches Gradle + the configuration cache between runs (big speedup).
29
+ - uses: gradle/actions/setup-gradle@v4
30
+
31
+ # Advisory toolchain report from create-cmp's doctor. Non-blocking:
32
+ # doctor also checks emulator/AVD bits that assembleDebug doesn't need,
33
+ # so a red doctor on a CI runner is informative, not fatal.
34
+ - name: Toolchain doctor (advisory)
35
+ continue-on-error: true
36
+ run: npx --yes create-cmp-cli@latest doctor --yes --no-install --no-ios
37
+
38
+ # The verify gate. If this fails after a dependency bump, remember:
39
+ # Kotlin / KSP / Compose / Room / AGP move as ONE set (see the comments
40
+ # in gradle/libs.versions.toml).
41
+ - name: Build Android (debug)
42
+ run: ./gradlew :composeApp:assembleDebug --stacktrace
43
+
44
+ # ── iOS (opt-in) ──────────────────────────────────────────────────────────
45
+ # Uncomment to build the iOS app on every push to main. Uses the exact
46
+ # command set create-cmp verified this template with: XcodeGen + CocoaPods
47
+ # (LANG must be UTF-8) + an arm64-only simulator build (the Kotlin framework
48
+ # embed phase emits only the active arch — a generic destination fails).
49
+ #
50
+ # ios:
51
+ # runs-on: macos-14
52
+ # if: github.event_name == 'push' && github.ref == 'refs/heads/main'
53
+ # steps:
54
+ # - uses: actions/checkout@v4
55
+ # - uses: actions/setup-java@v4
56
+ # with:
57
+ # distribution: temurin
58
+ # java-version: 17
59
+ # - uses: gradle/actions/setup-gradle@v4
60
+ # - run: brew install xcodegen # CocoaPods is preinstalled on macos-14
61
+ # - name: Build iOS (simulator)
62
+ # run: |
63
+ # cd iosApp
64
+ # export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
65
+ # xcodegen generate
66
+ # pod install
67
+ # SIM_UDID=$(xcrun simctl list devices available | grep -Eo '[0-9A-F-]{36}' | head -1)
68
+ # xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator \
69
+ # -configuration Debug ARCHS=arm64 ONLY_ACTIVE_ARCH=YES EXCLUDED_ARCHS=x86_64 \
70
+ # -destination "id=$SIM_UDID" build
@@ -0,0 +1,17 @@
1
+ plugins {
2
+ alias(libs.plugins.kotlin.multiplatform) apply false
3
+ alias(libs.plugins.kotlin.serialization) apply false
4
+ alias(libs.plugins.compose.multiplatform) apply false
5
+ alias(libs.plugins.compose.compiler) apply false
6
+ alias(libs.plugins.android.application) apply false
7
+ // >>> cmp:feature firebase
8
+ alias(libs.plugins.google.services) apply false
9
+ // <<< cmp:feature firebase
10
+ // >>> cmp:feature room
11
+ alias(libs.plugins.ksp) apply false
12
+ alias(libs.plugins.room) apply false
13
+ // <<< cmp:feature room
14
+ // >>> cmp:feature dev-client
15
+ alias(libs.plugins.compose.hot.reload) apply false
16
+ // <<< cmp:feature dev-client
17
+ }