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,283 @@
1
+ // Per-dependency detection + install-command definitions for the toolchain.
2
+ //
3
+ // Each check is: { id, label, platforms, detect() -> {present, detail},
4
+ // installCommand(ctx) -> string|null, manual?: string }.
5
+ // Detection uses real probes (which/--version/xcode-select/sdkmanager/appium…).
6
+ // installCommand returns the EXACT command to run; if null, the dep can't be
7
+ // auto-installed (e.g. Xcode → App Store) and `manual` explains the step.
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import os from "node:os";
12
+ import { probe, which } from "./exec.mjs";
13
+
14
+ const isMac = process.platform === "darwin";
15
+ const isLinux = process.platform === "linux";
16
+
17
+ function brewPrefixCommand(pkg) {
18
+ if (isMac) return `brew install ${pkg}`;
19
+ if (isLinux) return null;
20
+ return null;
21
+ }
22
+
23
+ function androidHome() {
24
+ return (
25
+ process.env.ANDROID_HOME ||
26
+ process.env.ANDROID_SDK_ROOT ||
27
+ path.join(os.homedir(), "Library", "Android", "sdk")
28
+ );
29
+ }
30
+
31
+ function sdkmanagerPath() {
32
+ const home = androidHome();
33
+ const candidates = [
34
+ path.join(home, "cmdline-tools", "latest", "bin", "sdkmanager"),
35
+ path.join(home, "cmdline-tools", "bin", "sdkmanager"),
36
+ path.join(home, "tools", "bin", "sdkmanager"),
37
+ ];
38
+ for (const c of candidates) if (fs.existsSync(c)) return c;
39
+ return which("sdkmanager");
40
+ }
41
+
42
+ function avdmanagerPath() {
43
+ const home = androidHome();
44
+ const candidates = [
45
+ path.join(home, "cmdline-tools", "latest", "bin", "avdmanager"),
46
+ path.join(home, "cmdline-tools", "bin", "avdmanager"),
47
+ ];
48
+ for (const c of candidates) if (fs.existsSync(c)) return c;
49
+ return which("avdmanager");
50
+ }
51
+
52
+ function emulatorPath() {
53
+ const home = androidHome();
54
+ const c = path.join(home, "emulator", "emulator");
55
+ if (fs.existsSync(c)) return c;
56
+ return which("emulator");
57
+ }
58
+
59
+ export const checks = [
60
+ {
61
+ id: "node",
62
+ label: "Node.js (≥18)",
63
+ platforms: ["darwin", "linux", "win32"],
64
+ detect() {
65
+ const r = probe("node", ["--version"]);
66
+ if (!r.ok) return { present: false, detail: "not found" };
67
+ const major = parseInt(r.stdout.replace(/^v/, "").split(".")[0], 10);
68
+ return { present: major >= 18, detail: r.stdout + (major >= 18 ? "" : " (need ≥18)") };
69
+ },
70
+ installCommand: () => (isMac ? "brew install node" : "see https://nodejs.org / use nvm"),
71
+ },
72
+ {
73
+ id: "jdk",
74
+ label: "JDK 17 (Temurin)",
75
+ platforms: ["darwin", "linux"],
76
+ detect() {
77
+ const r = probe("javac", ["-version"]);
78
+ const r2 = r.ok ? r : probe("java", ["-version"]);
79
+ const out = (r2.stdout || r2.stderr || "").trim();
80
+ const m = out.match(/(\d+)(\.\d+)?/);
81
+ const major = m ? parseInt(m[1], 10) : 0;
82
+ if (!out) return { present: false, detail: "not found" };
83
+ return { present: major >= 17, detail: out.split("\n")[0] };
84
+ },
85
+ installCommand: () =>
86
+ isMac ? "brew install --cask temurin@17" : "sdk install java 17.0.13-tem # (sdkman)",
87
+ },
88
+ {
89
+ id: "android-cmdline-tools",
90
+ label: "Android cmdline-tools (sdkmanager)",
91
+ platforms: ["darwin", "linux"],
92
+ detect() {
93
+ const sm = sdkmanagerPath();
94
+ return sm
95
+ ? { present: true, detail: sm }
96
+ : { present: false, detail: `not found under ${androidHome()}` };
97
+ },
98
+ installCommand: () =>
99
+ isMac
100
+ ? "brew install --cask android-commandlinetools"
101
+ : "download cmdline-tools from developer.android.com and unzip into $ANDROID_HOME/cmdline-tools/latest",
102
+ },
103
+ {
104
+ id: "android-platform",
105
+ label: "Android SDK platform + build-tools + platform-tools",
106
+ platforms: ["darwin", "linux"],
107
+ detect() {
108
+ const sm = sdkmanagerPath();
109
+ if (!sm) return { present: false, detail: "sdkmanager missing" };
110
+ const r = probe(sm, ["--list_installed"]);
111
+ const listing = r.ok ? r.stdout : probe(sm, ["--list"]).stdout;
112
+ const hasPlatform = /platforms;android-\d+/.test(listing);
113
+ const hasBuildTools = /build-tools;/.test(listing);
114
+ const hasPlatformTools = /platform-tools/.test(listing) || !!which("adb");
115
+ const present = hasPlatform && hasBuildTools && hasPlatformTools;
116
+ return {
117
+ present,
118
+ detail: present ? "platform+build-tools+platform-tools present" : "missing SDK components",
119
+ };
120
+ },
121
+ installCommand: () => {
122
+ const sm = sdkmanagerPath() || "sdkmanager";
123
+ return `"${sm}" "platform-tools" "platforms;android-35" "build-tools;35.0.0"`;
124
+ },
125
+ },
126
+ {
127
+ id: "android-system-image",
128
+ label: "Android system image",
129
+ platforms: ["darwin", "linux"],
130
+ detect() {
131
+ const sm = sdkmanagerPath();
132
+ if (!sm) return { present: false, detail: "sdkmanager missing" };
133
+ const r = probe(sm, ["--list_installed"]);
134
+ const listing = r.ok ? r.stdout : "";
135
+ const present = /system-images;android-\d+;/.test(listing);
136
+ return { present, detail: present ? "system image present" : "no system image installed" };
137
+ },
138
+ installCommand: () => {
139
+ const sm = sdkmanagerPath() || "sdkmanager";
140
+ const arch = process.arch === "arm64" ? "arm64-v8a" : "x86_64";
141
+ return `"${sm}" "system-images;android-35;google_apis;${arch}"`;
142
+ },
143
+ },
144
+ {
145
+ id: "android-avd",
146
+ label: "Bootable AVD",
147
+ platforms: ["darwin", "linux"],
148
+ detect() {
149
+ const em = emulatorPath();
150
+ if (em) {
151
+ const r = probe(em, ["-list-avds"]);
152
+ if (r.ok && r.stdout.trim().length > 0) {
153
+ return { present: true, detail: r.stdout.split("\n").join(", ") };
154
+ }
155
+ }
156
+ // Fallback: look for ~/.android/avd/*.avd
157
+ const avdDir = path.join(os.homedir(), ".android", "avd");
158
+ if (fs.existsSync(avdDir)) {
159
+ const avds = fs.readdirSync(avdDir).filter((f) => f.endsWith(".avd"));
160
+ if (avds.length) return { present: true, detail: avds.join(", ") };
161
+ }
162
+ return { present: false, detail: "no AVD found" };
163
+ },
164
+ installCommand: () => {
165
+ const am = avdmanagerPath() || "avdmanager";
166
+ const arch = process.arch === "arm64" ? "arm64-v8a" : "x86_64";
167
+ return `echo no | "${am}" create avd -n cmp_pixel -k "system-images;android-35;google_apis;${arch}" --device pixel_6`;
168
+ },
169
+ },
170
+ {
171
+ id: "adb",
172
+ label: "adb (platform-tools)",
173
+ platforms: ["darwin", "linux", "win32"],
174
+ detect() {
175
+ const adb = which("adb") || (() => {
176
+ const c = path.join(androidHome(), "platform-tools", "adb");
177
+ return fs.existsSync(c) ? c : null;
178
+ })();
179
+ return adb ? { present: true, detail: adb } : { present: false, detail: "adb not on PATH" };
180
+ },
181
+ installCommand: () => {
182
+ const sm = sdkmanagerPath() || "sdkmanager";
183
+ return `"${sm}" "platform-tools"`;
184
+ },
185
+ },
186
+ {
187
+ id: "xcode",
188
+ label: "Xcode + Command Line Tools",
189
+ platforms: ["darwin"],
190
+ detect() {
191
+ const r = probe("xcode-select", ["-p"]);
192
+ if (!r.ok || !r.stdout) return { present: false, detail: "xcode-select path not set" };
193
+ const isFullXcode = /Xcode\.app/.test(r.stdout);
194
+ return {
195
+ present: isFullXcode,
196
+ detail: r.stdout + (isFullXcode ? "" : " (only CLT — full Xcode needed for iOS build)"),
197
+ };
198
+ },
199
+ // Xcode itself can't be CLI-installed.
200
+ installCommand: () => null,
201
+ manual:
202
+ "Install Xcode from the App Store, then run `sudo xcode-select -s /Applications/Xcode.app` and `sudo xcodebuild -license accept`.",
203
+ },
204
+ {
205
+ id: "cocoapods",
206
+ label: "CocoaPods",
207
+ platforms: ["darwin"],
208
+ detect() {
209
+ const r = probe("pod", ["--version"]);
210
+ return r.ok
211
+ ? { present: true, detail: `pod ${r.stdout}` }
212
+ : { present: false, detail: "not found" };
213
+ },
214
+ installCommand: () => "brew install cocoapods",
215
+ },
216
+ {
217
+ id: "xcodegen",
218
+ label: "XcodeGen",
219
+ platforms: ["darwin"],
220
+ detect() {
221
+ const r = probe("xcodegen", ["--version"]);
222
+ return r.ok
223
+ ? { present: true, detail: r.stdout.split("\n")[0] }
224
+ : { present: false, detail: "not found" };
225
+ },
226
+ installCommand: () => "brew install xcodegen",
227
+ },
228
+ {
229
+ id: "appium",
230
+ label: "Appium 3.x",
231
+ platforms: ["darwin", "linux"],
232
+ detect() {
233
+ const r = probe("appium", ["--version"]);
234
+ if (!r.ok) return { present: false, detail: "not found" };
235
+ const major = parseInt(r.stdout.split(".")[0], 10);
236
+ return { present: major >= 3, detail: `appium ${r.stdout}${major >= 3 ? "" : " (need 3.x)"}` };
237
+ },
238
+ installCommand: () => "npm install -g appium@latest",
239
+ },
240
+ {
241
+ id: "appium-uiautomator2",
242
+ label: "Appium driver: uiautomator2 (Android)",
243
+ platforms: ["darwin", "linux"],
244
+ detect() {
245
+ const r = probe("appium", ["driver", "list", "--installed"]);
246
+ const out = (r.stdout || r.stderr || "").toLowerCase();
247
+ const present = out.includes("uiautomator2");
248
+ return { present, detail: present ? "installed" : "not installed" };
249
+ },
250
+ installCommand: () => "appium driver install uiautomator2",
251
+ },
252
+ {
253
+ id: "appium-xcuitest",
254
+ label: "Appium driver: xcuitest (iOS)",
255
+ platforms: ["darwin"],
256
+ detect() {
257
+ const r = probe("appium", ["driver", "list", "--installed"]);
258
+ const out = (r.stdout || r.stderr || "").toLowerCase();
259
+ const present = out.includes("xcuitest");
260
+ return { present, detail: present ? "installed" : "not installed" };
261
+ },
262
+ installCommand: () => "appium driver install xcuitest",
263
+ },
264
+ ];
265
+
266
+ /**
267
+ * Filter checks to those relevant to the current OS, optionally Android-only.
268
+ * @param {object} opts
269
+ * @param {boolean} [opts.iosWanted] include iOS-only checks (macOS only)
270
+ * @returns {Array}
271
+ */
272
+ export function checksForHost({ iosWanted = true } = {}) {
273
+ const plat = process.platform;
274
+ return checks.filter((c) => {
275
+ if (!c.platforms.includes(plat)) return false;
276
+ // iOS-specific checks are skipped off macOS or when iOS not wanted.
277
+ const iosOnly = c.platforms.length === 1 && c.platforms[0] === "darwin";
278
+ if (iosOnly && (!isMac || !iosWanted)) return false;
279
+ return true;
280
+ });
281
+ }
282
+
283
+ export const hostInfo = { isMac, isLinux, androidHome };
@@ -0,0 +1,90 @@
1
+ // Shared command-detection + consent-gated install helpers for the bootstrap
2
+ // toolchain installer. Detection is real (which/--version probes); every
3
+ // mutating install command is consent-gated and dry-runnable.
4
+
5
+ import { spawnSync, spawn } from "node:child_process";
6
+ import readline from "node:readline";
7
+
8
+ /**
9
+ * Run a command and capture output synchronously (for detection probes).
10
+ * Never throws — returns {ok, code, stdout, stderr}.
11
+ * @param {string} cmd
12
+ * @param {string[]} args
13
+ * @returns {{ok:boolean, code:number, stdout:string, stderr:string}}
14
+ */
15
+ export function probe(cmd, args = []) {
16
+ try {
17
+ const r = spawnSync(cmd, args, { encoding: "utf8", timeout: 20000 });
18
+ return {
19
+ ok: r.status === 0,
20
+ code: r.status ?? 1,
21
+ stdout: (r.stdout || "").trim(),
22
+ stderr: (r.stderr || "").trim(),
23
+ };
24
+ } catch {
25
+ return { ok: false, code: 127, stdout: "", stderr: "" };
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Is a binary on PATH?
31
+ * @param {string} bin
32
+ * @returns {string|null} resolved path or null
33
+ */
34
+ export function which(bin) {
35
+ const finder = process.platform === "win32" ? "where" : "which";
36
+ const r = probe(finder, [bin]);
37
+ if (r.ok && r.stdout) return r.stdout.split("\n")[0].trim();
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Ask the user for yes/no consent. In --yes mode, auto-approves.
43
+ * @param {string} question
44
+ * @param {object} opts
45
+ * @param {boolean} opts.assumeYes
46
+ * @returns {Promise<boolean>}
47
+ */
48
+ export async function consent(question, { assumeYes = false } = {}) {
49
+ if (assumeYes) {
50
+ process.stdout.write(`${question} ${"(auto-yes)"}\n`);
51
+ return true;
52
+ }
53
+ if (!process.stdin.isTTY) {
54
+ // Non-interactive without --yes: decline mutating actions.
55
+ process.stdout.write(`${question} (no TTY, declining — pass --yes to auto-approve)\n`);
56
+ return false;
57
+ }
58
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
59
+ const answer = await new Promise((resolve) => {
60
+ rl.question(`${question} [y/N] `, (a) => resolve(a.trim().toLowerCase()));
61
+ });
62
+ rl.close();
63
+ return answer === "y" || answer === "yes";
64
+ }
65
+
66
+ /**
67
+ * Run an install command, consent-gated and dry-runnable.
68
+ * @param {object} params
69
+ * @param {string} params.command human/shell install command
70
+ * @param {boolean} params.assumeYes
71
+ * @param {boolean} params.dryRun
72
+ * @returns {Promise<{ran:boolean, code:number, approved:boolean}>}
73
+ */
74
+ export async function runInstall({ command, assumeYes, dryRun }) {
75
+ if (dryRun) {
76
+ process.stdout.write(` [dry-run] would run: ${command}\n`);
77
+ return { ran: false, code: 0, approved: false };
78
+ }
79
+ const approved = await consent(` Run install: \`${command}\`?`, { assumeYes });
80
+ if (!approved) {
81
+ process.stdout.write(` skipped (not approved)\n`);
82
+ return { ran: false, code: 0, approved: false };
83
+ }
84
+ const code = await new Promise((resolve) => {
85
+ const child = spawn(command, { shell: true, stdio: "inherit", env: process.env });
86
+ child.on("close", (c) => resolve(c ?? 1));
87
+ child.on("error", () => resolve(127));
88
+ });
89
+ return { ran: true, code, approved: true };
90
+ }
@@ -0,0 +1,195 @@
1
+ // `create-cmp clean` — report-then-clean cache hygiene (disk-space failures
2
+ // are a real KMP build-killer).
3
+ //
4
+ // create-cmp clean [--target-dir .] [--yes] [--dry-run]
5
+ //
6
+ // What it does:
7
+ // - ~/.konan: reports total size; deletes ONLY clearly-stale toolchains —
8
+ // kotlin-native(-prebuilt) dirs whose version ≠ the project's kotlin
9
+ // version (no known project kotlin → report only).
10
+ // - project: deletes Gradle `build/` dirs (only those next to a
11
+ // build.gradle[.kts]) and the root `.gradle/` dir.
12
+ // - ~/.gradle/caches: size REPORT ONLY — never auto-deleted; the manual
13
+ // command is printed instead.
14
+ // - Sizes are shown before and after; every deletion is consent-gated
15
+ // (--yes skips the prompt, --dry-run never deletes).
16
+
17
+ import fs from "node:fs";
18
+ import os from "node:os";
19
+ import path from "node:path";
20
+
21
+ import { colors, ok, warn } from "../lib/log.mjs";
22
+ import { probe, consent } from "../bootstrap/exec.mjs";
23
+ import { selectStaleKonan, selectProjectCleanDirs } from "../lib/clean.mjs";
24
+ import { formatBytes } from "../lib/project-doctor.mjs";
25
+ import { parseVersions } from "../lib/toml.mjs";
26
+
27
+ function duBytes(p) {
28
+ if (!fs.existsSync(p)) return null;
29
+ const r = probe("du", ["-sk", p]);
30
+ if (!r.ok) return null;
31
+ const kb = parseInt(r.stdout.split(/\s+/)[0], 10);
32
+ return Number.isFinite(kb) ? kb * 1024 : null;
33
+ }
34
+
35
+ function listProjectTree(projectDir) {
36
+ const dirs = [];
37
+ const files = [];
38
+ const walk = (dir, rel, depth) => {
39
+ if (depth > 4) return; // Gradle module build dirs live shallow
40
+ let entries;
41
+ try {
42
+ entries = fs.readdirSync(dir, { withFileTypes: true });
43
+ } catch {
44
+ return;
45
+ }
46
+ for (const e of entries) {
47
+ const r = rel ? `${rel}/${e.name}` : e.name;
48
+ if (e.isDirectory()) {
49
+ dirs.push(r);
50
+ // don't descend into the heavy dirs we might delete, or vendored trees
51
+ if (e.name === "build" || e.name === ".gradle" || e.name === "node_modules" || e.name === ".git") continue;
52
+ walk(path.join(dir, e.name), r, depth + 1);
53
+ } else if (e.isFile()) {
54
+ files.push(r);
55
+ }
56
+ }
57
+ };
58
+ walk(projectDir, "", 0);
59
+ return { dirs, files };
60
+ }
61
+
62
+ function projectKotlinVersion(projectDir) {
63
+ try {
64
+ const toml = fs.readFileSync(path.join(projectDir, "gradle", "libs.versions.toml"), "utf8");
65
+ const kotlin = parseVersions(toml).get("kotlin");
66
+ return kotlin ? kotlin.value : null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * @param {Record<string,string|boolean>} flags
74
+ * @param {string|undefined} positional optional target dir positional
75
+ */
76
+ export async function runClean(flags, positional) {
77
+ const targetDir =
78
+ (typeof flags["target-dir"] === "string" && flags["target-dir"]) || positional || ".";
79
+ const projectDir = path.resolve(targetDir);
80
+ const dryRun = flags["dry-run"] === true;
81
+
82
+ process.stdout.write(`\n${colors.bold("create-cmp clean")} — cache & build-output hygiene\n\n`);
83
+
84
+ // ---- gather the plan ------------------------------------------------------
85
+ const plan = []; // {label, absPath, bytes}
86
+
87
+ // ~/.konan
88
+ const konanDir = path.join(os.homedir(), ".konan");
89
+ const konanBytes = duBytes(konanDir);
90
+ const kotlinVersion = projectKotlinVersion(projectDir);
91
+ if (konanBytes !== null) {
92
+ process.stdout.write(
93
+ `${colors.bold("~/.konan")}: ${formatBytes(konanBytes)}` +
94
+ (kotlinVersion
95
+ ? colors.dim(` (project kotlin: ${kotlinVersion})`)
96
+ : colors.dim(" (no project kotlin version found — nothing is provably stale)")) +
97
+ "\n"
98
+ );
99
+ let entries = [];
100
+ try {
101
+ entries = fs.readdirSync(konanDir).filter((e) => {
102
+ try {
103
+ return fs.statSync(path.join(konanDir, e)).isDirectory();
104
+ } catch {
105
+ return false;
106
+ }
107
+ });
108
+ } catch {
109
+ /* unreadable — skip */
110
+ }
111
+ const { stale, kept } = selectStaleKonan(entries, kotlinVersion);
112
+ for (const name of stale) {
113
+ const abs = path.join(konanDir, name);
114
+ plan.push({ label: `~/.konan/${name} (stale toolchain)`, absPath: abs, bytes: duBytes(abs) });
115
+ }
116
+ for (const k of kept) {
117
+ process.stdout.write(` ${colors.dim(`keep ${k.name} — ${k.reason}`)}\n`);
118
+ }
119
+ } else {
120
+ process.stdout.write(`${colors.bold("~/.konan")}: ${colors.dim("not present")}\n`);
121
+ }
122
+
123
+ // project build outputs
124
+ if (fs.existsSync(projectDir)) {
125
+ const tree = listProjectTree(projectDir);
126
+ const cleanDirs = selectProjectCleanDirs(tree);
127
+ for (const rel of cleanDirs) {
128
+ const abs = path.join(projectDir, rel);
129
+ plan.push({ label: `${path.basename(projectDir)}/${rel}`, absPath: abs, bytes: duBytes(abs) });
130
+ }
131
+ if (cleanDirs.length === 0) {
132
+ process.stdout.write(`${colors.bold("project")}: ${colors.dim(`no Gradle build/.gradle dirs under ${projectDir}`)}\n`);
133
+ }
134
+ }
135
+
136
+ // ~/.gradle/caches — REPORT ONLY
137
+ const gradleCaches = path.join(os.homedir(), ".gradle", "caches");
138
+ const gradleCachesBytes = duBytes(gradleCaches);
139
+ if (gradleCachesBytes !== null) {
140
+ process.stdout.write(
141
+ `${colors.bold("~/.gradle/caches")}: ${formatBytes(gradleCachesBytes)} ` +
142
+ colors.dim("(report only — never auto-deleted; reclaim manually with: rm -rf ~/.gradle/caches)") +
143
+ "\n"
144
+ );
145
+ }
146
+
147
+ if (plan.length === 0) {
148
+ ok("Nothing safe to clean.");
149
+ process.exit(0);
150
+ }
151
+
152
+ // ---- report the plan ------------------------------------------------------
153
+ const totalBytes = plan.reduce((s, p) => s + (p.bytes ?? 0), 0);
154
+ process.stdout.write(`\n${colors.bold("Would delete")} (${formatBytes(totalBytes)} total):\n`);
155
+ for (const p of plan) {
156
+ process.stdout.write(
157
+ ` ${colors.red("×")} ${p.label} ${colors.dim(p.bytes !== null ? `(${formatBytes(p.bytes)})` : "")}\n`
158
+ );
159
+ }
160
+
161
+ if (dryRun) {
162
+ process.stdout.write(`\n${colors.yellow("Dry run")} — nothing deleted.\n`);
163
+ process.exit(0);
164
+ }
165
+
166
+ const approved = await consent(`\nDelete the ${plan.length} item(s) above?`, {
167
+ assumeYes: flags.yes === true,
168
+ });
169
+ if (!approved) {
170
+ warn("Not approved — nothing deleted.");
171
+ process.exit(0);
172
+ }
173
+
174
+ // ---- delete + after sizes ---------------------------------------------------
175
+ let freed = 0;
176
+ for (const p of plan) {
177
+ try {
178
+ fs.rmSync(p.absPath, { recursive: true, force: true });
179
+ freed += p.bytes ?? 0;
180
+ ok(`deleted ${p.label}`);
181
+ } catch (err) {
182
+ warn(`could not delete ${p.label}: ${err.message}`);
183
+ }
184
+ }
185
+
186
+ const konanAfter = duBytes(konanDir);
187
+ process.stdout.write(
188
+ `\n${colors.green("Done.")} Freed ~${formatBytes(freed)}.` +
189
+ (konanBytes !== null && konanAfter !== null
190
+ ? ` ~/.konan: ${formatBytes(konanBytes)} → ${formatBytes(konanAfter)}.`
191
+ : "") +
192
+ "\n"
193
+ );
194
+ process.exit(0);
195
+ }