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.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/bin/create-cmp.mjs +91 -0
- package/options.schema.json +110 -0
- package/package.json +52 -0
- package/src/bootstrap/checks.mjs +283 -0
- package/src/bootstrap/exec.mjs +90 -0
- package/src/commands/clean.mjs +195 -0
- package/src/commands/create.mjs +223 -0
- package/src/commands/doctor.mjs +256 -0
- package/src/commands/upgrade.mjs +196 -0
- package/src/commands/verify.mjs +94 -0
- package/src/doctor.mjs +109 -0
- package/src/lib/args.mjs +38 -0
- package/src/lib/clean.mjs +72 -0
- package/src/lib/fsutil.mjs +90 -0
- package/src/lib/log.mjs +40 -0
- package/src/lib/project-doctor.mjs +329 -0
- package/src/lib/registry.mjs +102 -0
- package/src/lib/rename.mjs +116 -0
- package/src/lib/schema.mjs +123 -0
- package/src/lib/toggle.mjs +130 -0
- package/src/lib/tokens.mjs +96 -0
- package/src/lib/toml.mjs +137 -0
- package/src/lib/upgrade.mjs +159 -0
- package/src/lib/verify.mjs +97 -0
- package/src/scaffold.mjs +347 -0
- package/src/verify.mjs +4 -0
- package/src/versions/registry.json +42 -0
- package/template/.github/workflows/verify.yml +70 -0
- package/template/build.gradle.kts +17 -0
- package/template/composeApp/build.gradle.kts +257 -0
- package/template/composeApp/google-services.json +29 -0
- package/template/composeApp/proguard-rules.pro +34 -0
- package/template/composeApp/src/androidDebug/AndroidManifest.xml +13 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/ComposeRootRegistry.kt +40 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorCatalog.kt +80 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt +322 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +16 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/LiveSemanticsJson.kt +102 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/RemoteControlPage.kt +106 -0
- package/template/composeApp/src/androidDebug/res/xml/debug_network_security_config.xml +9 -0
- package/template/composeApp/src/androidMain/AndroidManifest.xml +28 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/AppApplication.kt +72 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/MainActivity.kt +24 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +42 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/data/local/DatabaseBuilder.android.kt +13 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/di/AndroidModule.kt +9 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/presentation/components/TestTagAutomation.android.kt +11 -0
- package/template/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml +9 -0
- package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/values/colors.xml +6 -0
- package/template/composeApp/src/androidMain/res/values/themes.xml +9 -0
- package/template/composeApp/src/androidMain/res/values-v31/themes.xml +8 -0
- package/template/composeApp/src/androidRelease/kotlin/com/example/app/inspector/InspectorInit.kt +13 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Bold.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Medium.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Regular.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_SemiBold.ttf +0 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/App.kt +7 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +7 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/AppDatabase.kt +28 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/DatabaseBuilder.kt +14 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/ItemDao.kt +18 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/FirebaseConfig.kt +5 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt +22 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +23 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/model/Item.kt +8 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/repository/ItemRepository.kt +8 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt +12 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/App.kt +13 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/BaseScreen.kt +69 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/TestTagAutomation.kt +18 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +39 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +87 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +35 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +43 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +144 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppTab.kt +25 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt +15 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +43 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/DesignToken.kt +25 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Shape.kt +12 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Theme.kt +64 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Tokens.kt +19 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Typography.kt +36 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.desktop.kt +9 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/data/local/DatabaseBuilder.desktop.kt +13 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/di/DesktopModule.kt +32 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/main.kt +29 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/presentation/components/TestTagAutomation.desktop.kt +6 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/KoinHelper.kt +50 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/MainViewController.kt +10 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +32 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/data/local/DatabaseBuilder.ios.kt +10 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/presentation/components/TestTagAutomation.ios.kt +11 -0
- package/template/docs/dev-client.md +52 -0
- package/template/gitignore +21 -0
- package/template/gradle/libs.versions.toml +100 -0
- package/template/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/template/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/template/gradle.properties +9 -0
- package/template/gradlew +152 -0
- package/template/gradlew.bat +94 -0
- package/template/iosApp/Podfile +27 -0
- package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png +0 -0
- package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/template/iosApp/iosApp/Assets.xcassets/Contents.json +6 -0
- package/template/iosApp/iosApp/ContentView.swift +17 -0
- package/template/iosApp/iosApp/GoogleService-Info.plist +34 -0
- package/template/iosApp/iosApp/Info.plist +43 -0
- package/template/iosApp/iosApp/PrivacyInfo.xcprivacy +42 -0
- package/template/iosApp/iosApp/iOSApp.swift +33 -0
- package/template/iosApp/project.yml +56 -0
- package/template/local.properties.example +2 -0
- package/template/manifest.json +125 -0
- package/template/qa/appium/README.md +23 -0
- package/template/qa/appium/lib/appium-client.mjs +225 -0
- package/template/qa/appium/package.json +8 -0
- package/template/qa/appium/run-android-smoke.mjs +39 -0
- package/template/settings.gradle.kts +34 -0
- package/template/tests/appium/cmp/conftest.py +96 -0
- package/template/tests/appium/cmp/test_smoke.py +17 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// `create-cmp create` (the DEFAULT command) — scaffold a new project.
|
|
2
|
+
// Moved verbatim from bin/create-cmp.mjs when the bin became a thin
|
|
3
|
+
// dispatcher; behavior and flags are unchanged.
|
|
4
|
+
|
|
5
|
+
import { flagBool } from "../lib/args.mjs";
|
|
6
|
+
|
|
7
|
+
// --- name helpers ------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function parseTabs(str) {
|
|
10
|
+
if (!str || str === true) return null;
|
|
11
|
+
const tabs = String(str)
|
|
12
|
+
.split(",")
|
|
13
|
+
.map((pair) => pair.trim())
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.map((pair) => {
|
|
16
|
+
const [label, icon] = pair.split(":").map((s) => s.trim());
|
|
17
|
+
return { label, icon: icon || label.toLowerCase() };
|
|
18
|
+
});
|
|
19
|
+
return tabs.length ? tabs : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pascalFromName(name) {
|
|
23
|
+
return (name || "App")
|
|
24
|
+
.replace(/[^A-Za-z0-9 ]/g, " ")
|
|
25
|
+
.split(/\s+/)
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
28
|
+
.join("") || "App";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function slugFromName(name) {
|
|
32
|
+
return (name || "app").toLowerCase().replace(/[^a-z0-9]/g, "") || "app";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- config builders -------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
function buildConfigFromFlags(flags, positional) {
|
|
38
|
+
const name = typeof flags.name === "string" ? flags.name : "MyApp";
|
|
39
|
+
const pkg = typeof flags.package === "string" ? flags.package : `com.${slugFromName(name)}.app`;
|
|
40
|
+
const ios = flagBool(flags, "ios", true);
|
|
41
|
+
const firebase = flagBool(flags, "firebase", true);
|
|
42
|
+
const targetDir =
|
|
43
|
+
(typeof flags["target-dir"] === "string" && flags["target-dir"]) ||
|
|
44
|
+
positional ||
|
|
45
|
+
`./${slugFromName(name)}`;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
appName: name,
|
|
49
|
+
package: pkg,
|
|
50
|
+
iosBundleId: typeof flags["bundle-id"] === "string" ? flags["bundle-id"] : pkg,
|
|
51
|
+
region: typeof flags.region === "string" ? flags.region : "us-central1",
|
|
52
|
+
themePrefix:
|
|
53
|
+
typeof flags["theme-prefix"] === "string" ? flags["theme-prefix"] : pascalFromName(name),
|
|
54
|
+
platforms: { android: true, ios },
|
|
55
|
+
firebase: {
|
|
56
|
+
enabled: firebase,
|
|
57
|
+
auth: typeof flags.auth === "string" ? flags.auth : "both",
|
|
58
|
+
firestore: flagBool(flags, "firestore", firebase),
|
|
59
|
+
storage: flagBool(flags, "storage", firebase),
|
|
60
|
+
functions: flagBool(flags, "functions", firebase),
|
|
61
|
+
fcm: flagBool(flags, "fcm", firebase),
|
|
62
|
+
},
|
|
63
|
+
room: flagBool(flags, "room", true),
|
|
64
|
+
appium: flagBool(flags, "appium", true),
|
|
65
|
+
inspector: flagBool(flags, "inspector", true),
|
|
66
|
+
devClient: flagBool(flags, "dev-client", true),
|
|
67
|
+
tabs: parseTabs(flags.tabs) || [
|
|
68
|
+
{ label: "Home", icon: "home" },
|
|
69
|
+
{ label: "Profile", icon: "person" },
|
|
70
|
+
],
|
|
71
|
+
targetDir,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function interactiveConfig(positional) {
|
|
76
|
+
let prompts;
|
|
77
|
+
try {
|
|
78
|
+
prompts = (await import("prompts")).default;
|
|
79
|
+
} catch {
|
|
80
|
+
process.stderr.write(
|
|
81
|
+
"Interactive mode needs the 'prompts' package. Re-run with flags + --yes for non-interactive use.\n"
|
|
82
|
+
);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const onCancel = () => {
|
|
87
|
+
process.stdout.write("Cancelled.\n");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const base = await prompts(
|
|
92
|
+
[
|
|
93
|
+
{ type: "text", name: "appName", message: "App display name", initial: "MyApp" },
|
|
94
|
+
{
|
|
95
|
+
type: "text",
|
|
96
|
+
name: "package",
|
|
97
|
+
message: "Package (reverse-DNS)",
|
|
98
|
+
initial: (prev) => `com.${slugFromName(prev)}.app`,
|
|
99
|
+
},
|
|
100
|
+
{ type: "confirm", name: "ios", message: "Enable iOS target?", initial: true },
|
|
101
|
+
{ type: "text", name: "region", message: "Firebase region", initial: "us-central1" },
|
|
102
|
+
{ type: "confirm", name: "firebase", message: "Wire Firebase (GitLive)?", initial: true },
|
|
103
|
+
],
|
|
104
|
+
{ onCancel }
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
let auth = "none";
|
|
108
|
+
let firestore = false, storage = false, functions = false, fcm = false;
|
|
109
|
+
if (base.firebase) {
|
|
110
|
+
const fb = await prompts(
|
|
111
|
+
[
|
|
112
|
+
{
|
|
113
|
+
type: "select",
|
|
114
|
+
name: "auth",
|
|
115
|
+
message: "Auth type",
|
|
116
|
+
choices: [
|
|
117
|
+
{ title: "Email + Phone (both)", value: "both" },
|
|
118
|
+
{ title: "Email only", value: "email" },
|
|
119
|
+
{ title: "Phone/OTP only", value: "phone" },
|
|
120
|
+
{ title: "None", value: "none" },
|
|
121
|
+
],
|
|
122
|
+
initial: 0,
|
|
123
|
+
},
|
|
124
|
+
{ type: "confirm", name: "firestore", message: "Firestore?", initial: true },
|
|
125
|
+
{ type: "confirm", name: "storage", message: "Storage?", initial: true },
|
|
126
|
+
{ type: "confirm", name: "functions", message: "Cloud Functions?", initial: true },
|
|
127
|
+
{ type: "confirm", name: "fcm", message: "FCM (push)?", initial: true },
|
|
128
|
+
],
|
|
129
|
+
{ onCancel }
|
|
130
|
+
);
|
|
131
|
+
auth = fb.auth; firestore = fb.firestore; storage = fb.storage;
|
|
132
|
+
functions = fb.functions; fcm = fb.fcm;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const extras = await prompts(
|
|
136
|
+
[
|
|
137
|
+
{ type: "confirm", name: "room", message: "Room local cache?", initial: true },
|
|
138
|
+
{ type: "confirm", name: "appium", message: "Appium test harness?", initial: true },
|
|
139
|
+
{
|
|
140
|
+
type: "confirm",
|
|
141
|
+
name: "inspector",
|
|
142
|
+
message: "Live on-device inspector (debug builds only)?",
|
|
143
|
+
initial: true,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: "confirm",
|
|
147
|
+
name: "devClient",
|
|
148
|
+
message: "Desktop dev-client (JVM window + Compose Hot Reload)?",
|
|
149
|
+
initial: true,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
name: "tabs",
|
|
154
|
+
message: "Bottom-nav tabs (label:icon, comma-separated)",
|
|
155
|
+
initial: "Home:home,Profile:person",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
name: "targetDir",
|
|
160
|
+
message: "Target directory",
|
|
161
|
+
initial: positional || `./${slugFromName(base.appName)}`,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
{ onCancel }
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
appName: base.appName,
|
|
169
|
+
package: base.package,
|
|
170
|
+
iosBundleId: base.package,
|
|
171
|
+
region: base.region,
|
|
172
|
+
themePrefix: pascalFromName(base.appName),
|
|
173
|
+
platforms: { android: true, ios: base.ios },
|
|
174
|
+
firebase: { enabled: base.firebase, auth, firestore, storage, functions, fcm },
|
|
175
|
+
room: extras.room,
|
|
176
|
+
appium: extras.appium,
|
|
177
|
+
inspector: extras.inspector,
|
|
178
|
+
devClient: extras.devClient,
|
|
179
|
+
tabs: parseTabs(extras.tabs) || [{ label: "Home", icon: "home" }],
|
|
180
|
+
targetDir: extras.targetDir,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- command ----------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Run the scaffold (default) command.
|
|
188
|
+
* @param {Record<string,string|boolean>} flags
|
|
189
|
+
* @param {string|undefined} positional first non-subcommand positional (target dir)
|
|
190
|
+
*/
|
|
191
|
+
export async function runCreate(flags, positional) {
|
|
192
|
+
const { scaffold } = await import("../scaffold.mjs");
|
|
193
|
+
|
|
194
|
+
const nonInteractive =
|
|
195
|
+
flags.yes === true ||
|
|
196
|
+
typeof flags.name === "string" ||
|
|
197
|
+
typeof flags.package === "string" ||
|
|
198
|
+
!process.stdin.isTTY;
|
|
199
|
+
|
|
200
|
+
const config = nonInteractive
|
|
201
|
+
? buildConfigFromFlags(flags, positional)
|
|
202
|
+
: await interactiveConfig(positional);
|
|
203
|
+
|
|
204
|
+
const verify = flagBool(flags, "verify", true);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const { verdict } = await scaffold(config, {
|
|
208
|
+
verify,
|
|
209
|
+
dryRunVerify: flags["dry-run-verify"] === true,
|
|
210
|
+
force: flags.force === true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (verify && verdict && !verdict.green) {
|
|
214
|
+
process.stderr.write("\nScaffold produced files but the verify gate did NOT go green.\n");
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
process.stdout.write(`\nDone. cd ${config.targetDir}\n`);
|
|
218
|
+
process.exit(0);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
process.stderr.write(`\nError: ${err.message}\n`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// `create-cmp doctor` — toolchain preflight (existing behavior, preserved) PLUS
|
|
2
|
+
// a project-diagnosis section that activates when run inside (or pointed at,
|
|
3
|
+
// via --target-dir) a directory with Gradle files. The project section works on
|
|
4
|
+
// ANY KMP project — not just ones we scaffolded.
|
|
5
|
+
//
|
|
6
|
+
// create-cmp doctor [--yes] [--dry-run] [--no-ios] [--no-install]
|
|
7
|
+
// [--target-dir <dir>] [--fix]
|
|
8
|
+
//
|
|
9
|
+
// --fix applies only SAFE heals (write local.properties from ANDROID_HOME, add
|
|
10
|
+
// ksp.useKSP2=true); everything else prints the exact manual step.
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
import { flagBool } from "../lib/args.mjs";
|
|
17
|
+
import { colors, ok } from "../lib/log.mjs";
|
|
18
|
+
import { probe } from "../bootstrap/exec.mjs";
|
|
19
|
+
import { doctor as toolchainDoctor } from "../doctor.mjs";
|
|
20
|
+
import { diagnoseProject } from "../lib/project-doctor.mjs";
|
|
21
|
+
import { parseProperties, upsertProperty, parseVersions } from "../lib/toml.mjs";
|
|
22
|
+
import { loadRegistry } from "../lib/registry.mjs";
|
|
23
|
+
import { listFiles } from "../lib/fsutil.mjs";
|
|
24
|
+
|
|
25
|
+
function readIfExists(p) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.readFileSync(p, "utf8");
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Does this dir look like a Gradle project? (activates project diagnosis) */
|
|
34
|
+
export function isGradleProjectDir(dir) {
|
|
35
|
+
return (
|
|
36
|
+
fs.existsSync(path.join(dir, "settings.gradle.kts")) ||
|
|
37
|
+
fs.existsSync(path.join(dir, "settings.gradle")) ||
|
|
38
|
+
fs.existsSync(path.join(dir, "build.gradle.kts")) ||
|
|
39
|
+
fs.existsSync(path.join(dir, "build.gradle")) ||
|
|
40
|
+
fs.existsSync(path.join(dir, "gradle", "libs.versions.toml"))
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function androidHomeDir() {
|
|
45
|
+
const envHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
46
|
+
if (envHome && fs.existsSync(envHome)) return envHome;
|
|
47
|
+
const conventional = path.join(os.homedir(), "Library", "Android", "sdk");
|
|
48
|
+
if (fs.existsSync(conventional)) return conventional;
|
|
49
|
+
const linux = path.join(os.homedir(), "Android", "Sdk");
|
|
50
|
+
if (fs.existsSync(linux)) return linux;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** ~/.konan size in bytes via `du -sk` (null when absent/unknown). */
|
|
55
|
+
function konanSizeBytes() {
|
|
56
|
+
const konan = path.join(os.homedir(), ".konan");
|
|
57
|
+
if (!fs.existsSync(konan)) return null;
|
|
58
|
+
const r = probe("du", ["-sk", konan]);
|
|
59
|
+
if (!r.ok) return null;
|
|
60
|
+
const kb = parseInt(r.stdout.split(/\s+/)[0], 10);
|
|
61
|
+
return Number.isFinite(kb) ? kb * 1024 : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Free disk bytes for the home volume (null when undeterminable). */
|
|
65
|
+
function freeDiskBytes() {
|
|
66
|
+
try {
|
|
67
|
+
const s = fs.statfsSync(os.homedir());
|
|
68
|
+
return Number(s.bavail) * Number(s.bsize);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Gather filesystem/env inputs for the pure diagnosis. */
|
|
75
|
+
export function gatherProjectInputs(projectDir) {
|
|
76
|
+
const toml = readIfExists(path.join(projectDir, "gradle", "libs.versions.toml"));
|
|
77
|
+
const gradleProperties = readIfExists(path.join(projectDir, "gradle.properties"));
|
|
78
|
+
const localProperties = readIfExists(path.join(projectDir, "local.properties"));
|
|
79
|
+
|
|
80
|
+
let sdkDirExists = null;
|
|
81
|
+
if (localProperties !== null) {
|
|
82
|
+
const sdkDir = parseProperties(localProperties).get("sdk.dir");
|
|
83
|
+
if (sdkDir) {
|
|
84
|
+
// local.properties escapes ':' and '\' on some platforms.
|
|
85
|
+
const raw = sdkDir.value.replace(/\\(.)/g, "$1");
|
|
86
|
+
sdkDirExists = fs.existsSync(raw);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const hasIos =
|
|
91
|
+
fs.existsSync(path.join(projectDir, "iosApp")) ||
|
|
92
|
+
fs.existsSync(path.join(projectDir, "composeApp", "src", "iosMain"));
|
|
93
|
+
|
|
94
|
+
let registry = null;
|
|
95
|
+
try {
|
|
96
|
+
registry = loadRegistry();
|
|
97
|
+
} catch {
|
|
98
|
+
// corrupt registry should not break doctor — drift check is skipped
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { inspectorHits, inspectorCatalog } = scanInspectorSources(projectDir);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
toml,
|
|
105
|
+
gradleProperties,
|
|
106
|
+
localProperties,
|
|
107
|
+
sdkDirExists,
|
|
108
|
+
androidHomeSet: androidHomeDir() !== null,
|
|
109
|
+
hasIos,
|
|
110
|
+
registry,
|
|
111
|
+
konanBytes: konanSizeBytes(),
|
|
112
|
+
freeDiskBytes: freeDiskBytes(),
|
|
113
|
+
inspectorHits,
|
|
114
|
+
inspectorCatalog,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Static scan for the live-inspector release-safety check: which Kotlin sources
|
|
120
|
+
* reference the inspector endpoint, and (for the catalog drift tripwire) the
|
|
121
|
+
* stamped InspectorCatalog.kt + theme sources. Purely filesystem — doctor never
|
|
122
|
+
* probes the network for this (the release guarantee is structural).
|
|
123
|
+
*/
|
|
124
|
+
function scanInspectorSources(projectDir) {
|
|
125
|
+
const srcRoot = path.join(projectDir, "composeApp", "src");
|
|
126
|
+
if (!fs.existsSync(srcRoot)) return { inspectorHits: null, inspectorCatalog: null };
|
|
127
|
+
|
|
128
|
+
const hits = [];
|
|
129
|
+
let catalog = null;
|
|
130
|
+
let theme = "";
|
|
131
|
+
for (const file of listFiles(srcRoot)) {
|
|
132
|
+
if (!file.endsWith(".kt")) continue;
|
|
133
|
+
const content = readIfExists(file);
|
|
134
|
+
if (content === null) continue;
|
|
135
|
+
const rel = path.relative(projectDir, file).split(path.sep).join("/");
|
|
136
|
+
if (content.includes("/inspect/") || content.includes("InspectorHttpServer")) {
|
|
137
|
+
hits.push(rel);
|
|
138
|
+
}
|
|
139
|
+
if (path.basename(file) === "InspectorCatalog.kt") catalog = content;
|
|
140
|
+
if (/\/presentation\/theme\/(Tokens|Theme)\.kt$/.test(rel)) theme += `${content}\n`;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
inspectorHits: hits,
|
|
144
|
+
inspectorCatalog: catalog && theme ? { catalog, theme } : null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Apply the SAFE auto-heals for --fix. Returns ids of findings it fixed. */
|
|
149
|
+
function applySafeFixes(projectDir, findings, inputs) {
|
|
150
|
+
const fixed = [];
|
|
151
|
+
for (const f of findings) {
|
|
152
|
+
if (!f.fix || !f.fix.auto || f.level === "ok") continue;
|
|
153
|
+
|
|
154
|
+
if (f.id === "local-properties") {
|
|
155
|
+
const sdk = androidHomeDir();
|
|
156
|
+
if (!sdk) continue;
|
|
157
|
+
const target = path.join(projectDir, "local.properties");
|
|
158
|
+
const existing = readIfExists(target) ?? "";
|
|
159
|
+
const { content, changed } = upsertProperty(existing, "sdk.dir", sdk);
|
|
160
|
+
if (changed) {
|
|
161
|
+
fs.writeFileSync(target, content);
|
|
162
|
+
ok(`--fix: wrote sdk.dir=${sdk} to local.properties`);
|
|
163
|
+
fixed.push(f.id);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (f.id === "ksp2-flag") {
|
|
168
|
+
const target = path.join(projectDir, "gradle.properties");
|
|
169
|
+
const existing = inputs.gradleProperties ?? "";
|
|
170
|
+
const { content, changed } = upsertProperty(existing, "ksp.useKSP2", "true");
|
|
171
|
+
if (changed) {
|
|
172
|
+
fs.writeFileSync(target, content);
|
|
173
|
+
ok(`--fix: set ksp.useKSP2=true in gradle.properties`);
|
|
174
|
+
fixed.push(f.id);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return fixed;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function printFindings(findings) {
|
|
182
|
+
for (const f of findings) {
|
|
183
|
+
if (f.level === "ok") {
|
|
184
|
+
process.stdout.write(`${colors.green("✓")} ${f.title} — ${colors.dim(f.detail)}\n`);
|
|
185
|
+
} else if (f.level === "warn") {
|
|
186
|
+
process.stdout.write(`${colors.yellow("!")} ${f.title}\n ${colors.dim(f.detail)}\n`);
|
|
187
|
+
if (f.fix) process.stdout.write(` ${colors.cyan(f.fix.auto ? "fix (--fix):" : "fix:")} ${f.fix.description}\n`);
|
|
188
|
+
} else {
|
|
189
|
+
process.stdout.write(`${colors.red("✗")} ${f.title}\n ${colors.dim(f.detail)}\n`);
|
|
190
|
+
if (f.fix) process.stdout.write(` ${colors.cyan(f.fix.auto ? "fix (--fix):" : "fix:")} ${f.fix.description}\n`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {Record<string,string|boolean>} flags
|
|
197
|
+
* @param {string|undefined} positional optional target dir positional
|
|
198
|
+
*/
|
|
199
|
+
export async function runDoctor(flags, positional) {
|
|
200
|
+
// 1) Toolchain preflight — unchanged existing behavior.
|
|
201
|
+
const toolchain = await toolchainDoctor({
|
|
202
|
+
assumeYes: flags.yes === true,
|
|
203
|
+
dryRun: flags["dry-run"] === true,
|
|
204
|
+
ios: flagBool(flags, "ios", true),
|
|
205
|
+
installMissing: flags["no-install"] !== true,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// 2) Project diagnosis — only when pointed at / run inside a Gradle project.
|
|
209
|
+
const targetDir =
|
|
210
|
+
(typeof flags["target-dir"] === "string" && flags["target-dir"]) || positional || ".";
|
|
211
|
+
const projectDir = path.resolve(targetDir);
|
|
212
|
+
|
|
213
|
+
let projectGreen = true;
|
|
214
|
+
if (isGradleProjectDir(projectDir)) {
|
|
215
|
+
process.stdout.write(
|
|
216
|
+
`\n${colors.bold("Project diagnosis")} — ${colors.cyan(projectDir)}\n` +
|
|
217
|
+
`${colors.dim("(works on any KMP project — not only create-cmp-scaffolded ones)")}\n\n`
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
let inputs = gatherProjectInputs(projectDir);
|
|
221
|
+
let findings = diagnoseProject(inputs);
|
|
222
|
+
|
|
223
|
+
if (flags.fix === true) {
|
|
224
|
+
const fixed = applySafeFixes(projectDir, findings, inputs);
|
|
225
|
+
if (fixed.length > 0) {
|
|
226
|
+
// Re-diagnose so the report reflects the healed state.
|
|
227
|
+
inputs = gatherProjectInputs(projectDir);
|
|
228
|
+
findings = diagnoseProject(inputs);
|
|
229
|
+
} else {
|
|
230
|
+
process.stdout.write(`${colors.dim("--fix: nothing auto-fixable found.")}\n`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
printFindings(findings);
|
|
235
|
+
projectGreen = !findings.some((f) => f.level === "fail");
|
|
236
|
+
|
|
237
|
+
if (!flags.fix && findings.some((f) => f.fix?.auto && f.level !== "ok")) {
|
|
238
|
+
process.stdout.write(
|
|
239
|
+
`\n${colors.cyan("Tip:")} re-run with ${colors.bold("--fix")} to apply the safe heals above automatically.\n`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (inputs.toml) {
|
|
243
|
+
const kotlin = parseVersions(inputs.toml).get("kotlin");
|
|
244
|
+
if (kotlin) {
|
|
245
|
+
process.stdout.write(`${colors.dim(`Project kotlin: ${kotlin.value}`)}\n`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
process.stdout.write(
|
|
249
|
+
projectGreen
|
|
250
|
+
? `\n${colors.green("Project diagnosis: no blocking issues.")}\n`
|
|
251
|
+
: `\n${colors.red("Project diagnosis: blocking issues found (see ✗ above).")}\n`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
process.exit(toolchain.green && projectGreen ? 0 : 1);
|
|
256
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// `create-cmp upgrade` — migrate an existing Gradle/KMP project to the next
|
|
2
|
+
// PROVEN-GREEN version set.
|
|
3
|
+
//
|
|
4
|
+
// create-cmp upgrade [--target-dir .] [--set <id>] [--dry-run] [--yes] [--verify]
|
|
5
|
+
//
|
|
6
|
+
// Behavior:
|
|
7
|
+
// - Parses gradle/libs.versions.toml [versions], diffs against the target
|
|
8
|
+
// registry set (default: latest), prints a change table.
|
|
9
|
+
// - Default is a DRY RUN unless confirmed: applying requires --yes or an
|
|
10
|
+
// interactive "y". --dry-run never applies.
|
|
11
|
+
// - Applying rewrites ONLY the changed version values in-place (surgical
|
|
12
|
+
// line edits — formatting/comments preserved), updates gradle.properties
|
|
13
|
+
// flags the set requires (ksp.useKSP2), and the wrapper distributionUrl.
|
|
14
|
+
// - Every touched file gets a `<file>.bak-upgrade` backup first, and the
|
|
15
|
+
// revert commands are printed.
|
|
16
|
+
// - Lockstep guardrail: refuses to write a file where ksp is not
|
|
17
|
+
// `<kotlin>-…`.
|
|
18
|
+
// - Works on ANY project with a libs.versions.toml — template markers only
|
|
19
|
+
// soften/strengthen messaging, never refuse.
|
|
20
|
+
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
|
|
24
|
+
import { flagBool } from "../lib/args.mjs";
|
|
25
|
+
import { colors, ok, warn, fail, step } from "../lib/log.mjs";
|
|
26
|
+
import { consent } from "../bootstrap/exec.mjs";
|
|
27
|
+
import { loadRegistry, latestSet, getSet } from "../lib/registry.mjs";
|
|
28
|
+
import { planUpgrade, BACKUP_SUFFIX } from "../lib/upgrade.mjs";
|
|
29
|
+
|
|
30
|
+
function readIfExists(p) {
|
|
31
|
+
try {
|
|
32
|
+
return fs.readFileSync(p, "utf8");
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printDiffTable(changes) {
|
|
39
|
+
const keyWidth = Math.max(...changes.map((c) => c.key.length), 7);
|
|
40
|
+
const fromWidth = Math.max(...changes.map((c) => c.from.length), 7);
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
` ${colors.bold("version".padEnd(keyWidth))} ${colors.bold("current".padEnd(fromWidth))} ${colors.bold("target")}\n`
|
|
43
|
+
);
|
|
44
|
+
for (const c of changes) {
|
|
45
|
+
process.stdout.write(
|
|
46
|
+
` ${c.key.padEnd(keyWidth)} ${colors.red(c.from.padEnd(fromWidth))} ${colors.dim("→")} ${colors.green(c.to)}\n`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {Record<string,string|boolean>} flags
|
|
53
|
+
* @param {string|undefined} positional optional target dir positional
|
|
54
|
+
*/
|
|
55
|
+
export async function runUpgrade(flags, positional) {
|
|
56
|
+
const targetDir =
|
|
57
|
+
(typeof flags["target-dir"] === "string" && flags["target-dir"]) || positional || ".";
|
|
58
|
+
const projectDir = path.resolve(targetDir);
|
|
59
|
+
|
|
60
|
+
const tomlPath = path.join(projectDir, "gradle", "libs.versions.toml");
|
|
61
|
+
const tomlContent = readIfExists(tomlPath);
|
|
62
|
+
if (tomlContent === null) {
|
|
63
|
+
process.stderr.write(
|
|
64
|
+
`Error: no gradle/libs.versions.toml under ${projectDir}.\n` +
|
|
65
|
+
`\`create-cmp upgrade\` works on any Gradle project that uses a version catalog — run it from the project root or pass --target-dir.\n`
|
|
66
|
+
);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const registry = loadRegistry();
|
|
71
|
+
let set;
|
|
72
|
+
if (typeof flags.set === "string") {
|
|
73
|
+
set = getSet(registry, flags.set);
|
|
74
|
+
if (!set) {
|
|
75
|
+
process.stderr.write(
|
|
76
|
+
`Error: unknown version set "${flags.set}". Available: ${registry.sets.map((s) => s.id).join(", ")}\n`
|
|
77
|
+
);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
set = latestSet(registry);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const gradlePropsPath = path.join(projectDir, "gradle.properties");
|
|
85
|
+
const wrapperPropsPath = path.join(projectDir, "gradle", "wrapper", "gradle-wrapper.properties");
|
|
86
|
+
const plan = planUpgrade({
|
|
87
|
+
tomlContent,
|
|
88
|
+
gradlePropertiesContent: readIfExists(gradlePropsPath),
|
|
89
|
+
wrapperPropertiesContent: readIfExists(wrapperPropsPath),
|
|
90
|
+
set,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
process.stdout.write(
|
|
94
|
+
`\n${colors.bold("create-cmp upgrade")} — proven-green version set ${colors.cyan(set.id)}` +
|
|
95
|
+
(set.label ? ` ${colors.dim(`(${set.label})`)}` : "") +
|
|
96
|
+
`\n project: ${colors.cyan(projectDir)}\n` +
|
|
97
|
+
(plan.fromOurTemplate
|
|
98
|
+
? ` ${colors.dim("catalog carries the create-cmp frozen-set marker — this project was stamped by create-cmp.")}\n`
|
|
99
|
+
: ` ${colors.dim("not a create-cmp-stamped catalog — that's fine, upgrade works on any version catalog; review the diff extra carefully.")}\n`) +
|
|
100
|
+
"\n"
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Guardrail FIRST — never even offer to write a broken pairing.
|
|
104
|
+
if (plan.lockstepError) {
|
|
105
|
+
fail(`Lockstep guardrail: ${plan.lockstepError}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { changes, unmanaged, notInProject } = plan.diff;
|
|
110
|
+
if (changes.length === 0) {
|
|
111
|
+
ok(`Catalog already matches set ${set.id} — nothing to change.`);
|
|
112
|
+
} else {
|
|
113
|
+
printDiffTable(changes);
|
|
114
|
+
}
|
|
115
|
+
if (plan.propertyChanges.length > 0) {
|
|
116
|
+
process.stdout.write("\n");
|
|
117
|
+
for (const p of plan.propertyChanges) {
|
|
118
|
+
step(
|
|
119
|
+
`gradle.properties: ${p.key}=${p.to}` +
|
|
120
|
+
(p.from === null ? colors.dim(" (new)") : colors.dim(` (was ${p.from})`))
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (plan.wrapperChange) {
|
|
125
|
+
step(`gradle wrapper: ${plan.wrapperChange.from} ${colors.dim("→")} ${plan.wrapperChange.to}`);
|
|
126
|
+
}
|
|
127
|
+
if (unmanaged.length > 0) {
|
|
128
|
+
warn(
|
|
129
|
+
`Left untouched (not in set ${set.id}): ${unmanaged.map((u) => `${u.key} ${u.value}`).join(", ")}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (notInProject.length > 0) {
|
|
133
|
+
process.stdout.write(
|
|
134
|
+
colors.dim(` (set pins ${notInProject.join(", ")} — not declared by this project, nothing added)\n`)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(set.notes) && set.notes.length > 0) {
|
|
138
|
+
process.stdout.write(`\n${colors.bold("Set notes")}\n`);
|
|
139
|
+
for (const n of set.notes) process.stdout.write(` ${colors.dim("·")} ${n}\n`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const anythingToWrite =
|
|
143
|
+
plan.newTomlContent !== null ||
|
|
144
|
+
plan.newGradlePropertiesContent !== null ||
|
|
145
|
+
plan.newWrapperPropertiesContent !== null;
|
|
146
|
+
if (!anythingToWrite) {
|
|
147
|
+
ok("Project is fully aligned — nothing to apply.");
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (flags["dry-run"] === true) {
|
|
152
|
+
process.stdout.write(`\n${colors.yellow("Dry run")} — nothing written. Re-run with --yes to apply.\n`);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const approved = await consent(`\nApply these changes (backups written as *${BACKUP_SUFFIX})?`, {
|
|
157
|
+
assumeYes: flags.yes === true,
|
|
158
|
+
});
|
|
159
|
+
if (!approved) {
|
|
160
|
+
process.stdout.write(`${colors.yellow("Not applied")} — dry run only. Re-run with --yes to apply.\n`);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Apply, backing up each file before its first write.
|
|
165
|
+
const touched = [];
|
|
166
|
+
const writes = [
|
|
167
|
+
{ path: tomlPath, content: plan.newTomlContent },
|
|
168
|
+
{ path: gradlePropsPath, content: plan.newGradlePropertiesContent },
|
|
169
|
+
{ path: wrapperPropsPath, content: plan.newWrapperPropertiesContent },
|
|
170
|
+
];
|
|
171
|
+
for (const w of writes) {
|
|
172
|
+
if (w.content === null) continue;
|
|
173
|
+
fs.copyFileSync(w.path, w.path + BACKUP_SUFFIX);
|
|
174
|
+
fs.writeFileSync(w.path, w.content);
|
|
175
|
+
touched.push(w.path);
|
|
176
|
+
ok(`wrote ${path.relative(projectDir, w.path)} ${colors.dim(`(backup: ${path.relative(projectDir, w.path)}${BACKUP_SUFFIX})`)}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
process.stdout.write(`\n${colors.bold("To revert")}\n`);
|
|
180
|
+
for (const t of touched) {
|
|
181
|
+
process.stdout.write(` mv "${t}${BACKUP_SUFFIX}" "${t}"\n`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (flagBool(flags, "verify", false)) {
|
|
185
|
+
process.stdout.write("\n");
|
|
186
|
+
const { runVerifyCommand } = await import("./verify.mjs");
|
|
187
|
+
// runVerifyCommand exits the process with the gate's verdict.
|
|
188
|
+
await runVerifyCommand({ "target-dir": projectDir }, undefined);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
process.stdout.write(
|
|
193
|
+
`\n${colors.green("Applied.")} Prove the build: ${colors.cyan(`create-cmp verify --target-dir ${targetDir}`)}\n`
|
|
194
|
+
);
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|