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,94 @@
|
|
|
1
|
+
// `create-cmp verify` — run the green-build gate against an EXISTING project
|
|
2
|
+
// directory (the same north-star gate the scaffold ends on, exposed
|
|
3
|
+
// standalone). Usage: create-cmp verify [--target-dir .] [--no-ios] [--dry-run]
|
|
4
|
+
//
|
|
5
|
+
// Command resolution order:
|
|
6
|
+
// 1. <project>/manifest.json `verify` block (rare — the scaffold deletes it)
|
|
7
|
+
// 2. the bundled template's manifest.json `verify` block
|
|
8
|
+
// 3. hard fallbacks (./gradlew :composeApp:assembleDebug)
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
import { flagBool } from "../lib/args.mjs";
|
|
15
|
+
import { runVerify, printVerifyVerdict } from "../lib/verify.mjs";
|
|
16
|
+
import { colors } from "../lib/log.mjs";
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
|
20
|
+
|
|
21
|
+
const FALLBACK_VERIFY = {
|
|
22
|
+
android: "./gradlew :composeApp:assembleDebug",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Resolve the verify command block for a project dir. */
|
|
26
|
+
export function resolveVerifyCommands(projectDir) {
|
|
27
|
+
for (const candidate of [
|
|
28
|
+
path.join(projectDir, "manifest.json"),
|
|
29
|
+
path.join(REPO_ROOT, "template", "manifest.json"),
|
|
30
|
+
]) {
|
|
31
|
+
try {
|
|
32
|
+
const manifest = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
33
|
+
if (manifest && manifest.verify && manifest.verify.android) {
|
|
34
|
+
return { verify: manifest.verify, source: candidate };
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// keep falling through
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { verify: FALLBACK_VERIFY, source: "built-in fallback" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {Record<string,string|boolean>} flags
|
|
45
|
+
* @param {string|undefined} positional optional target dir positional
|
|
46
|
+
*/
|
|
47
|
+
export async function runVerifyCommand(flags, positional) {
|
|
48
|
+
const targetDir =
|
|
49
|
+
(typeof flags["target-dir"] === "string" && flags["target-dir"]) || positional || ".";
|
|
50
|
+
const projectDir = path.resolve(targetDir);
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(projectDir)) {
|
|
53
|
+
process.stderr.write(`Error: ${projectDir} does not exist.\n`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const looksGradle =
|
|
57
|
+
fs.existsSync(path.join(projectDir, "settings.gradle.kts")) ||
|
|
58
|
+
fs.existsSync(path.join(projectDir, "settings.gradle")) ||
|
|
59
|
+
fs.existsSync(path.join(projectDir, "gradlew"));
|
|
60
|
+
if (!looksGradle) {
|
|
61
|
+
process.stderr.write(
|
|
62
|
+
`Error: ${projectDir} does not look like a Gradle project (no settings.gradle[.kts] or gradlew).\n`
|
|
63
|
+
);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { verify, source } = resolveVerifyCommands(projectDir);
|
|
68
|
+
|
|
69
|
+
// iOS leg runs only when the project actually has an iOS shell (and the user
|
|
70
|
+
// didn't opt out); runVerify additionally gates on macOS.
|
|
71
|
+
const hasIosShell = fs.existsSync(path.join(projectDir, "iosApp"));
|
|
72
|
+
const ios = flagBool(flags, "ios", true) && hasIosShell;
|
|
73
|
+
|
|
74
|
+
process.stdout.write(
|
|
75
|
+
`${colors.bold("create-cmp verify")} — green-build gate\n` +
|
|
76
|
+
` project: ${colors.cyan(projectDir)}\n` +
|
|
77
|
+
` commands: ${colors.dim(source)}\n\n`
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const dryRun = flags["dry-run"] === true;
|
|
81
|
+
const verdict = await runVerify({
|
|
82
|
+
projectDir,
|
|
83
|
+
manifest: { verify },
|
|
84
|
+
config: { platforms: { ios } },
|
|
85
|
+
dryRun,
|
|
86
|
+
});
|
|
87
|
+
printVerifyVerdict(verdict);
|
|
88
|
+
if (dryRun) {
|
|
89
|
+
process.stdout.write(
|
|
90
|
+
`${colors.yellow("Dry run")} — commands printed, nothing executed; the build is NOT proven.\n`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
process.exit(verdict.green ? 0 : 1);
|
|
94
|
+
}
|
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// doctor → bootstrap → verify toolchain installer.
|
|
2
|
+
//
|
|
3
|
+
// Detects the CMP toolchain (JDK 17, Android SDK/cmdline-tools/system-image/AVD,
|
|
4
|
+
// Xcode+CLT, CocoaPods, XcodeGen, Node, Appium 3.x + uiautomator2/xcuitest, adb).
|
|
5
|
+
// For each missing dep: prints the EXACT install command and asks consent
|
|
6
|
+
// (--yes skips prompts for CI). Idempotent (safe to re-run). OS-aware
|
|
7
|
+
// (Linux → Android-only, said explicitly). Xcode can't be CLI-installed — the
|
|
8
|
+
// App Store step is surfaced as the one manual action. Ends with a
|
|
9
|
+
// per-dependency GREEN/FAIL verdict table.
|
|
10
|
+
|
|
11
|
+
import { checksForHost, hostInfo } from "./bootstrap/checks.mjs";
|
|
12
|
+
import { runInstall } from "./bootstrap/exec.mjs";
|
|
13
|
+
import { colors } from "./lib/log.mjs";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} opts
|
|
17
|
+
* @param {boolean} [opts.assumeYes] --yes: auto-approve installs (CI)
|
|
18
|
+
* @param {boolean} [opts.dryRun] print install commands, don't run them
|
|
19
|
+
* @param {boolean} [opts.ios] whether iOS support is wanted (gates iOS checks)
|
|
20
|
+
* @param {boolean} [opts.installMissing=true] attempt installs for missing deps
|
|
21
|
+
* @returns {Promise<{green:boolean, rows:Array}>}
|
|
22
|
+
*/
|
|
23
|
+
export async function doctor(opts = {}) {
|
|
24
|
+
const {
|
|
25
|
+
assumeYes = false,
|
|
26
|
+
dryRun = false,
|
|
27
|
+
ios = true,
|
|
28
|
+
installMissing = true,
|
|
29
|
+
} = opts;
|
|
30
|
+
|
|
31
|
+
process.stdout.write(`\n${colors.bold("create-cmp doctor")} — toolchain preflight\n`);
|
|
32
|
+
if (hostInfo.isLinux) {
|
|
33
|
+
process.stdout.write(
|
|
34
|
+
`${colors.yellow("Linux detected")}: scoping to Android-only (iOS toolchain is macOS-only).\n`
|
|
35
|
+
);
|
|
36
|
+
} else if (!hostInfo.isMac) {
|
|
37
|
+
process.stdout.write(
|
|
38
|
+
`${colors.yellow("Non-macOS host")}: iOS checks are skipped.\n`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
process.stdout.write("\n");
|
|
42
|
+
|
|
43
|
+
const hostChecks = checksForHost({ iosWanted: ios });
|
|
44
|
+
const rows = [];
|
|
45
|
+
|
|
46
|
+
for (const check of hostChecks) {
|
|
47
|
+
let { present, detail } = check.detect();
|
|
48
|
+
const cmd = present ? null : check.installCommand({});
|
|
49
|
+
|
|
50
|
+
if (present) {
|
|
51
|
+
process.stdout.write(`${colors.green("✓")} ${check.label} — ${colors.dim(detail)}\n`);
|
|
52
|
+
} else {
|
|
53
|
+
process.stdout.write(`${colors.red("✗")} ${check.label} — ${colors.dim(detail)}\n`);
|
|
54
|
+
if (check.manual && cmd === null) {
|
|
55
|
+
process.stdout.write(` ${colors.yellow("manual:")} ${check.manual}\n`);
|
|
56
|
+
} else if (cmd) {
|
|
57
|
+
process.stdout.write(` ${colors.cyan("fix:")} ${cmd}\n`);
|
|
58
|
+
if (installMissing) {
|
|
59
|
+
const res = await runInstall({ command: cmd, assumeYes, dryRun });
|
|
60
|
+
if (res.ran && res.code === 0) {
|
|
61
|
+
// Re-detect after install (idempotent verify).
|
|
62
|
+
const after = check.detect();
|
|
63
|
+
present = after.present;
|
|
64
|
+
detail = after.detail;
|
|
65
|
+
if (present) {
|
|
66
|
+
process.stdout.write(` ${colors.green("→ now present")} (${detail})\n`);
|
|
67
|
+
} else {
|
|
68
|
+
process.stdout.write(
|
|
69
|
+
` ${colors.yellow("→ still not detected")} (may need a new shell / PATH export)\n`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
} else if (res.ran && res.code !== 0) {
|
|
73
|
+
process.stdout.write(` ${colors.red(`install exited ${res.code}`)}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
rows.push({
|
|
80
|
+
id: check.id,
|
|
81
|
+
label: check.label,
|
|
82
|
+
present,
|
|
83
|
+
detail,
|
|
84
|
+
manual: !present && check.installCommand({}) === null,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Verdict table.
|
|
89
|
+
process.stdout.write(`\n${colors.bold("Verdict")}\n`);
|
|
90
|
+
let green = true;
|
|
91
|
+
for (const r of rows) {
|
|
92
|
+
if (r.present) {
|
|
93
|
+
process.stdout.write(` ${colors.green("GREEN")} ${r.label}\n`);
|
|
94
|
+
} else if (r.manual) {
|
|
95
|
+
process.stdout.write(` ${colors.yellow("MANUAL")} ${r.label}\n`);
|
|
96
|
+
green = false;
|
|
97
|
+
} else {
|
|
98
|
+
process.stdout.write(` ${colors.red("FAIL ")} ${r.label}\n`);
|
|
99
|
+
green = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
process.stdout.write(
|
|
103
|
+
green
|
|
104
|
+
? `\n${colors.green("GREEN — toolchain ready.")}\n`
|
|
105
|
+
: `\n${colors.red("FAIL — toolchain incomplete.")} Re-run after addressing the items above (idempotent).\n`
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return { green, rows };
|
|
109
|
+
}
|
package/src/lib/args.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Tiny shared arg parser for the CLI (moved verbatim from bin/create-cmp.mjs
|
|
2
|
+
// when the bin became a thin subcommand dispatcher). No new behavior.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse argv into positionals + flags. `--flag value` captures the value;
|
|
6
|
+
* `--flag` followed by another `--flag` (or nothing) is boolean true.
|
|
7
|
+
* @param {string[]} argv
|
|
8
|
+
* @returns {{_: string[], flags: Record<string, string|boolean>}}
|
|
9
|
+
*/
|
|
10
|
+
export function parseArgs(argv) {
|
|
11
|
+
const args = { _: [], flags: {} };
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const a = argv[i];
|
|
14
|
+
if (a.startsWith("--")) {
|
|
15
|
+
const key = a.slice(2);
|
|
16
|
+
const next = argv[i + 1];
|
|
17
|
+
if (next === undefined || next.startsWith("--")) {
|
|
18
|
+
args.flags[key] = true; // boolean flag
|
|
19
|
+
} else {
|
|
20
|
+
args.flags[key] = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
args._.push(a);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tri-state boolean flag: `--name`/`--name true` → true, `--no-name`/`--name false`
|
|
32
|
+
* → false, otherwise the default.
|
|
33
|
+
*/
|
|
34
|
+
export function flagBool(flags, name, dflt) {
|
|
35
|
+
if (flags[name] === true || flags[name] === "true") return true;
|
|
36
|
+
if (flags[`no-${name}`] === true || flags[name] === "false") return false;
|
|
37
|
+
return dflt;
|
|
38
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Pure selection logic for `create-cmp clean`. The command layer does the
|
|
2
|
+
// listing/sizing/deleting; this module decides WHAT is safe to remove, so the
|
|
3
|
+
// policy is unit-testable against fake directory listings.
|
|
4
|
+
//
|
|
5
|
+
// Policy (deliberately conservative):
|
|
6
|
+
// - ~/.konan: only "clearly stale" entries — kotlin-native toolchain dirs
|
|
7
|
+
// whose trailing version does NOT match the project's kotlin version.
|
|
8
|
+
// Shared dirs (dependencies/, cache/, kotlin-native-prebuilt-<current>)
|
|
9
|
+
// are never selected. No known project kotlin version → nothing is stale.
|
|
10
|
+
// - project: only `build/` dirs that sit next to a build.gradle(.kts) (i.e.
|
|
11
|
+
// real Gradle module outputs) plus the root `.gradle/` dir.
|
|
12
|
+
// - ~/.gradle/caches: REPORT ONLY, never auto-deleted.
|
|
13
|
+
|
|
14
|
+
/** kotlin-native toolchain dir with a trailing version, e.g.
|
|
15
|
+
* kotlin-native-prebuilt-macos-aarch64-2.1.20 */
|
|
16
|
+
const KONAN_TOOLCHAIN_RE = /^kotlin-native(?:-prebuilt)?(?:-[a-z0-9_]+)*?-(\d+\.\d+(?:\.\d+)?(?:-[A-Za-z0-9.]+)?)$/;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Which ~/.konan entries are clearly stale?
|
|
20
|
+
* @param {string[]} entries directory names inside ~/.konan
|
|
21
|
+
* @param {string|null|undefined} projectKotlinVersion the project's kotlin version
|
|
22
|
+
* @returns {{stale:string[], kept:Array<{name:string, reason:string}>}}
|
|
23
|
+
*/
|
|
24
|
+
export function selectStaleKonan(entries, projectKotlinVersion) {
|
|
25
|
+
const stale = [];
|
|
26
|
+
const kept = [];
|
|
27
|
+
for (const name of entries) {
|
|
28
|
+
const m = name.match(KONAN_TOOLCHAIN_RE);
|
|
29
|
+
if (!m) {
|
|
30
|
+
kept.push({ name, reason: "not a versioned kotlin-native toolchain dir (shared cache — never touched)" });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!projectKotlinVersion) {
|
|
34
|
+
kept.push({ name, reason: "project kotlin version unknown — cannot prove staleness" });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (m[1] === projectKotlinVersion) {
|
|
38
|
+
kept.push({ name, reason: `matches the project's kotlin ${projectKotlinVersion}` });
|
|
39
|
+
} else {
|
|
40
|
+
stale.push(name);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { stale, kept };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Which project-relative paths are safe Gradle outputs to delete?
|
|
48
|
+
* @param {object} input
|
|
49
|
+
* @param {string[]} input.dirs ALL directory paths in the project, RELATIVE with "/" separators
|
|
50
|
+
* @param {string[]} input.files ALL file paths in the project, RELATIVE with "/" separators
|
|
51
|
+
* @returns {string[]} relative dir paths safe to delete (root `.gradle` + module `build` dirs)
|
|
52
|
+
*/
|
|
53
|
+
export function selectProjectCleanDirs({ dirs, files }) {
|
|
54
|
+
const fileSet = new Set(files);
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const dir of dirs) {
|
|
57
|
+
if (dir === ".gradle") {
|
|
58
|
+
out.push(dir);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const segments = dir.split("/");
|
|
62
|
+
if (segments[segments.length - 1] !== "build") continue;
|
|
63
|
+
// Never reach inside another build dir or hidden/vendored trees.
|
|
64
|
+
if (segments.slice(0, -1).some((s) => s === "build" || s === "node_modules" || s.startsWith("."))) continue;
|
|
65
|
+
const parent = segments.slice(0, -1).join("/");
|
|
66
|
+
const sibling = (name) => (parent ? `${parent}/${name}` : name);
|
|
67
|
+
if (fileSet.has(sibling("build.gradle.kts")) || fileSet.has(sibling("build.gradle")) || fileSet.has(sibling("settings.gradle.kts")) || fileSet.has(sibling("settings.gradle"))) {
|
|
68
|
+
out.push(dir);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Filesystem helpers: recursive copy and a text-file walker.
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Recursively copy srcDir → destDir, creating dest dirs as needed.
|
|
8
|
+
* Symlinks are copied as symlinks. Existing dest files are overwritten.
|
|
9
|
+
* @param {string} srcDir
|
|
10
|
+
* @param {string} destDir
|
|
11
|
+
*/
|
|
12
|
+
export function copyDir(srcDir, destDir) {
|
|
13
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
14
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
15
|
+
const s = path.join(srcDir, entry.name);
|
|
16
|
+
const d = path.join(destDir, entry.name);
|
|
17
|
+
if (entry.isSymbolicLink()) {
|
|
18
|
+
const link = fs.readlinkSync(s);
|
|
19
|
+
try { fs.unlinkSync(d); } catch {}
|
|
20
|
+
fs.symlinkSync(link, d);
|
|
21
|
+
} else if (entry.isDirectory()) {
|
|
22
|
+
copyDir(s, d);
|
|
23
|
+
} else {
|
|
24
|
+
fs.copyFileSync(s, d);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Yield absolute paths of every regular file under dir (recursive).
|
|
31
|
+
* @param {string} dir
|
|
32
|
+
* @returns {string[]}
|
|
33
|
+
*/
|
|
34
|
+
export function listFiles(dir) {
|
|
35
|
+
const out = [];
|
|
36
|
+
const stack = [dir];
|
|
37
|
+
while (stack.length) {
|
|
38
|
+
const cur = stack.pop();
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = fs.readdirSync(cur, { withFileTypes: true });
|
|
42
|
+
} catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const e of entries) {
|
|
46
|
+
const full = path.join(cur, e.name);
|
|
47
|
+
if (e.isDirectory()) stack.push(full);
|
|
48
|
+
else if (e.isFile()) out.push(full);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Yield absolute paths of every directory under dir (recursive), DEEPEST FIRST
|
|
56
|
+
* (so path-token renames of nested dirs are safe to apply bottom-up).
|
|
57
|
+
* @param {string} dir
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
60
|
+
export function listDirsDeepestFirst(dir) {
|
|
61
|
+
const out = [];
|
|
62
|
+
const stack = [dir];
|
|
63
|
+
while (stack.length) {
|
|
64
|
+
const cur = stack.pop();
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = fs.readdirSync(cur, { withFileTypes: true });
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const e of entries) {
|
|
72
|
+
if (e.isDirectory()) {
|
|
73
|
+
const full = path.join(cur, e.name);
|
|
74
|
+
out.push(full);
|
|
75
|
+
stack.push(full);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Deepest first = longest path first.
|
|
80
|
+
out.sort((a, b) => b.length - a.length);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isMacOS() {
|
|
85
|
+
return process.platform === "darwin";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isLinux() {
|
|
89
|
+
return process.platform === "linux";
|
|
90
|
+
}
|
package/src/lib/log.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Thin logging helper with optional color. Falls back gracefully if picocolors
|
|
2
|
+
// isn't installed (so the engine still works zero-dep in a pinch).
|
|
3
|
+
|
|
4
|
+
let pc;
|
|
5
|
+
try {
|
|
6
|
+
pc = (await import("picocolors")).default;
|
|
7
|
+
} catch {
|
|
8
|
+
const identity = (s) => s;
|
|
9
|
+
pc = {
|
|
10
|
+
green: identity, red: identity, yellow: identity, cyan: identity,
|
|
11
|
+
dim: identity, bold: identity, blue: identity, magenta: identity,
|
|
12
|
+
gray: identity,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const colors = pc;
|
|
17
|
+
|
|
18
|
+
export function info(msg) {
|
|
19
|
+
process.stdout.write(`${msg}\n`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ok(msg) {
|
|
23
|
+
process.stdout.write(`${pc.green("✓")} ${msg}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function warn(msg) {
|
|
27
|
+
process.stdout.write(`${pc.yellow("!")} ${msg}\n`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function fail(msg) {
|
|
31
|
+
process.stdout.write(`${pc.red("✗")} ${msg}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function step(msg) {
|
|
35
|
+
process.stdout.write(`${pc.cyan("›")} ${msg}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function dim(msg) {
|
|
39
|
+
process.stdout.write(`${pc.dim(msg)}\n`);
|
|
40
|
+
}
|